From 9de57da0bdba32fe9bedfa07a0e6f019cd270e47 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 6 Oct 2026 12:40:05 +0200 Subject: [PATCH 01/11] Add BrotliDecompressionOptions for Large Window Brotli decompression --- .../Common/src/Interop/Interop.Brotli.cs | 3 + .../ref/System.IO.Compression.Brotli.cs | 7 ++ .../src/Resources/Strings.resx | 9 ++ .../src/System.IO.Compression.Brotli.csproj | 2 + .../src/System/IO/Compression/BrotliUtils.cs | 90 +++++++++++++++++++ .../IO/Compression/dec/BrotliDecoder.cs | 53 +++++++++++ .../Compression/dec/BrotliDecoderParameter.cs | 15 ++++ .../dec/BrotliDecompressionOptions.cs | 36 ++++++++ .../dec/BrotliStream.Decompress.cs | 12 +++ .../enc/BrotliCompressionOptions.cs | 7 +- .../IO/Compression/enc/BrotliEncoder.cs | 24 +++-- .../Compression/enc/BrotliEncoderParameter.cs | 4 +- .../Compression/enc/BrotliStream.Compress.cs | 3 + .../entrypoints.c | 1 + 14 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoderParameter.cs create mode 100644 src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs diff --git a/src/libraries/Common/src/Interop/Interop.Brotli.cs b/src/libraries/Common/src/Interop/Interop.Brotli.cs index 31d6a94d6fdde7..ad8ea99de62ec5 100644 --- a/src/libraries/Common/src/Interop/Interop.Brotli.cs +++ b/src/libraries/Common/src/Interop/Interop.Brotli.cs @@ -13,6 +13,9 @@ internal static partial class Brotli [LibraryImport(Libraries.CompressionNative)] internal static partial SafeBrotliDecoderHandle BrotliDecoderCreateInstance(IntPtr allocFunc, IntPtr freeFunc, IntPtr opaque); + [LibraryImport(Libraries.CompressionNative)] + internal static partial BOOL BrotliDecoderSetParameter(SafeBrotliDecoderHandle state, BrotliDecoderParameter parameter, uint value); + [LibraryImport(Libraries.CompressionNative)] internal static unsafe partial int BrotliDecoderDecompressStream( SafeBrotliDecoderHandle state, ref nuint availableIn, byte** nextIn, diff --git a/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs index b60adea4dfae3a..ef3af0f3faa4a2 100644 --- a/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs @@ -16,10 +16,16 @@ public partial struct BrotliDecoder : System.IDisposable { private object _dummy; private int _dummyPrimitive; + public BrotliDecoder(System.IO.Compression.BrotliDecompressionOptions decompressionOptions) { throw null; } 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 BrotliDecompressionOptions + { + public BrotliDecompressionOptions() { } + public int MaxWindowLog2 { get { throw null; } set { } } + } public partial struct BrotliEncoder : System.IDisposable { private object _dummy; @@ -35,6 +41,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.BrotliDecompressionOptions decompressionOptions, 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) { } diff --git a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx index 13b26dc8fdb4f4..c1e91bb374be2d 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx @@ -148,6 +148,9 @@ The BrotliEncoder {0} can not be changed at current encoder state. + + A window of {0} requires the Large Window Brotli extension, which the encoder does not use at the specified quality of {1}. Specify a quality greater than {2}. + Failed to create BrotliDecoder instance @@ -155,6 +158,12 @@ Can not access a closed Decoder. + + The BrotliDecoder {0} can not be changed at current decoder state. + + + The Brotli stream requires a window size of 2^{0} bytes, which is greater than the maxWindowLog2 of 2^{1} bytes allowed by the decompression options. + Can not perform Read operations on a BrotliStream constructed with CompressionMode.Compress. diff --git a/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj b/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj index 124cd52bc84993..dfe1b9a463e4a7 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj +++ b/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj @@ -19,6 +19,8 @@ + + diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs index 1a51983f2f0e70..ee5effd292ecca 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs @@ -1,6 +1,8 @@ // 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 { internal static partial class BrotliUtils @@ -8,9 +10,97 @@ internal static partial class BrotliUtils public const int WindowBits_Min = 10; public const int WindowBits_Default = 22; public const int WindowBits_Max = 24; + /// The maximum window size supported by the "Large Window Brotli" extension (BROTLI_LARGE_MAX_WINDOW_BITS). + public const int WindowBits_MaxLarge = 30; public const int Quality_Min = 0; public const int Quality_Default = 4; public const int Quality_Max = 11; + /// + /// The highest quality that uses static entropy codes (MAX_QUALITY_FOR_STATIC_ENTROPY_CODES). The native encoder silently + /// disables large-window mode at or below this quality, clamping the window back to . + /// + public const int Quality_MaxForStaticEntropyCodes = 2; + + /// + /// Reads the window size declared by the header of a Brotli stream without consuming any input. + /// + /// A buffer positioned at the start of a Brotli stream. + /// When this method returns , the declared base-2 logarithm of the window size. + /// + /// if the window size was determined; if + /// is too short to determine it; otherwise . + /// + /// + /// The header is read least-significant-bit first and occupies at most 14 bits. This mirrors DecodeWindowBits and the + /// BROTLI_STATE_LARGE_WINDOW_BITS state of the native decoder. + /// + internal static OperationStatus TryGetWindowBits(ReadOnlySpan source, out int windowBits) + { + // The header is a variable-length prefix, read least-significant-bit first: + // bit 0 == 0 -> window is 16 + // bits 1..3 (n) != 0 -> window is 17 + n (18..24) + // bits 4..6 (n) == 1 -> "Large Window Brotli" escape: bit 7 is reserved and must be zero, + // and bits 8..13 hold the window size (10..30) + // bits 4..6 (n) > 1 -> window is 8 + n (10..15) + // bits 4..6 (n) == 0 -> window is 17 + const int LargeWindowEscape = 1; + const int LargeWindowBitCount = 6; + + windowBits = 0; + + if (source.IsEmpty) + { + return OperationStatus.NeedMoreData; + } + + int header = source[0]; + if ((header & 0b1) == 0) + { + windowBits = 16; + return OperationStatus.Done; + } + + int value = (header >> 1) & 0b111; + if (value != 0) + { + windowBits = 17 + value; + return OperationStatus.Done; + } + + value = (header >> 4) & 0b111; + if (value != LargeWindowEscape) + { + windowBits = value != 0 ? 8 + value : 17; + return OperationStatus.Done; + } + + // The escape is followed by a reserved bit that must be zero, and then by the window size itself. + if ((header & 0b1000_0000) != 0) + { + return OperationStatus.InvalidData; + } + + if (source.Length < 2) + { + return OperationStatus.NeedMoreData; + } + + windowBits = source[1] & ((1 << LargeWindowBitCount) - 1); + return windowBits is < WindowBits_Min or > WindowBits_MaxLarge + ? OperationStatus.InvalidData + : OperationStatus.Done; + } + + /// + /// Validates that a large window size is paired with a quality that the native encoder will honor. + /// + internal static void ValidateLargeWindowQuality(int quality, int windowLog2) + { + if (windowLog2 > WindowBits_Max && quality <= Quality_MaxForStaticEntropyCodes) + { + throw new ArgumentException(SR.Format(SR.BrotliEncoder_LargeWindowQuality, windowLog2, quality, Quality_MaxForStaticEntropyCodes)); + } + } internal static int GetQualityFromCompressionLevel(CompressionLevel compressionLevel) => compressionLevel switch diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs index 8b6730ef523801..ac15c31f3875db 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs @@ -13,12 +13,45 @@ public struct BrotliDecoder : IDisposable { private SafeBrotliDecoderHandle? _state; private bool _disposed; + private int _maxWindowLog2; + private bool _windowValidated; + /// + /// Gets the maximum window size accepted by this decoder. A zero-initialized has no explicitly + /// configured value and defaults to the maximum defined by RFC 7932. + /// + private readonly int EffectiveMaxWindowLog2 => _maxWindowLog2 == 0 ? BrotliUtils.WindowBits_Max : _maxWindowLog2; + + + /// Initializes a new instance of the structure using the specified decompression options. + /// The Brotli options for fine tuning the decompression. + /// is . + public BrotliDecoder(BrotliDecompressionOptions decompressionOptions) + { + ArgumentNullException.ThrowIfNull(decompressionOptions); + + _maxWindowLog2 = decompressionOptions.MaxWindowLog2; + } + + internal void SetMaxWindowLog2(int maxWindowLog2) + { + Debug.Assert(_state is null, "The window size must be configured before the native decoder is created."); + + _maxWindowLog2 = maxWindowLog2; + } internal void InitializeDecoder() { _state = Interop.Brotli.BrotliDecoderCreateInstance(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (_state.IsInvalid) throw new IOException(SR.BrotliDecoder_Create); + + // The native decoder only distinguishes between the RFC 7932 window range and the "Large Window Brotli" range; any + // finer limit is enforced against the window declared by the stream header once decompression starts. + if (EffectiveMaxWindowLog2 > BrotliUtils.WindowBits_Max && + Interop.Brotli.BrotliDecoderSetParameter(_state, BrotliDecoderParameter.LargeWindow, 1) == Interop.BOOL.FALSE) + { + throw new InvalidOperationException(SR.Format(SR.BrotliDecoder_InvalidSetParameter, nameof(BrotliDecoderParameter.LargeWindow))); + } } internal void EnsureInitialized() @@ -47,6 +80,7 @@ private void EnsureNotDisposed() /// The total number of bytes that were read from . /// The total number of bytes that were written in the . /// One of the enumeration values that indicates the status of the decompression operation. + /// The window size declared by is greater than the configured maximum. /// The return value can be as follows: /// - : was successfully and completely decompressed into . /// - : There is not enough space in to decompress . @@ -59,6 +93,25 @@ public OperationStatus Decompress(ReadOnlySpan source, Span destinat bytesConsumed = 0; bytesWritten = 0; + + if (_maxWindowLog2 != 0 && !_windowValidated) + { + switch (BrotliUtils.TryGetWindowBits(source, out int windowBits)) + { + case OperationStatus.NeedMoreData: + return OperationStatus.NeedMoreData; + + case OperationStatus.Done when windowBits > _maxWindowLog2: + throw new IOException(SR.Format(SR.BrotliDecoder_MaxWindowLog2Exceeded, windowBits, _maxWindowLog2)); + + default: + // A malformed header is reported by the native decoder, so that the error matches what callers that did not + // specify any decompression options would observe. + _windowValidated = true; + break; + } + } + if (Interop.Brotli.BrotliDecoderIsFinished(_state) != Interop.BOOL.FALSE) return OperationStatus.Done; nuint availableOutput = (nuint)destination.Length; diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoderParameter.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoderParameter.cs new file mode 100644 index 00000000000000..5aa49949bbe6bf --- /dev/null +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoderParameter.cs @@ -0,0 +1,15 @@ +// 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 +{ + /// + /// DisableRingBufferReallocation - Disables the lazy growth of the ring buffer, so that the full window is allocated upfront. + /// LargeWindow - Flag that determines if "Large Window Brotli" is used, allowing window sizes up to 2^30 instead of the 2^24 maximum defined by RFC 7932. + /// + internal enum BrotliDecoderParameter + { + DisableRingBufferReallocation, + LargeWindow + } +} diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs new file mode 100644 index 00000000000000..3b6ff9992646aa --- /dev/null +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs @@ -0,0 +1,36 @@ +// 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 . + /// + public sealed class BrotliDecompressionOptions + { + private int _maxWindowLog2 = BrotliUtils.WindowBits_Max; + + /// + /// Gets or sets the maximum base-2 logarithm of the window size accepted when decompressing. + /// + /// The value is less than 10 or greater than 30. + /// + /// A Brotli stream declares the window size it requires in its header; this property does not select a window size, it + /// rejects streams that ask for more than the specified value. Range is from 10 to 30. The default value is 24, the maximum + /// defined by RFC 7932. Values greater than 24 additionally enable the "Large Window Brotli" extension, which is not part of + /// RFC 7932. Because the decoder only allocates the memory a stream actually uses, raising this value does not by itself + /// increase memory consumption; it raises the upper bound of what a stream is permitted to request. + /// + public int MaxWindowLog2 + { + get => _maxWindowLog2; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, BrotliUtils.WindowBits_Min, nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, BrotliUtils.WindowBits_MaxLarge, nameof(value)); + + _maxWindowLog2 = value; + } + } + } +} diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliStream.Decompress.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliStream.Decompress.cs index 4ac6f784ef08bf..5a4a19994a6374 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliStream.Decompress.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliStream.Decompress.cs @@ -18,6 +18,18 @@ public sealed partial class BrotliStream : Stream private bool _nonEmptyInput; private volatile bool _decompressionFinished; + /// Initializes a new instance of the class by using the specified stream and decompression options, and optionally leaves the stream open. + /// The stream from which data to decompress is read. + /// The Brotli options for fine tuning the decompression stream. + /// to leave the stream open after disposing the object; otherwise, . + /// or is . + public BrotliStream(Stream stream, BrotliDecompressionOptions decompressionOptions, bool leaveOpen = false) : this(stream, CompressionMode.Decompress, leaveOpen) + { + ArgumentNullException.ThrowIfNull(decompressionOptions); + + _decoder.SetMaxWindowLog2(decompressionOptions.MaxWindowLog2); + } + /// Reads a number of decompressed bytes into the specified byte array. /// The array used to store decompressed bytes. /// The byte offset in at which the read bytes will be placed. diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs index cb7f5322d05d1a..25dc772b581748 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs @@ -33,9 +33,10 @@ public int Quality /// /// Gets or sets the base-2 logarithm of the window size for a Brotli compression stream. /// - /// The value is less than 10 or greater than 24. + /// The value is less than 10 or greater than 30. /// - /// The value is expressed as the base-2 logarithm of the size in bytes of the sliding window used by the LZ77 algorithm. Larger window sizes can improve compression ratio but use more memory. Range is from 10 to 24. The default value is 22. + /// The value is expressed as the base-2 logarithm of the size in bytes of the sliding window used by the LZ77 algorithm. Larger window sizes can improve compression ratio but use more memory. Range is from 10 to 30. The default value is 22. + /// Values greater than 24 produce a stream that uses the "Large Window Brotli" extension, which is not part of RFC 7932 and which decoders must explicitly opt into. Such values also require a greater than 2. /// public int WindowLog2 { @@ -43,7 +44,7 @@ public int WindowLog2 set { ArgumentOutOfRangeException.ThrowIfLessThan(value, BrotliUtils.WindowBits_Min, nameof(value)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value, BrotliUtils.WindowBits_Max, nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, BrotliUtils.WindowBits_MaxLarge, nameof(value)); _windowLog2 = value; } diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs index e2f2b74ae38c3d..f5b85b404ae045 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs @@ -16,13 +16,16 @@ public partial struct BrotliEncoder : IDisposable /// Initializes a new instance of the structure using the specified quality and window. /// A number representing quality of the Brotli compression. 0 is the minimum (no compression), 11 is the maximum. - /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 24. + /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 30. /// is not between the minimum value of 0 and the maximum value of 11. /// -or- - /// is not between the minimum value of 10 and the maximum value of 24. + /// is not between the minimum value of 10 and the maximum value of 30. + /// is greater than 24, which requires the "Large Window Brotli" extension, but is too low for the encoder to use it. /// Failed to create the instance. public BrotliEncoder(int quality, int window) { + BrotliUtils.ValidateLargeWindowQuality(quality, window); + _disposed = false; _state = Interop.Brotli.BrotliEncoderCreateInstance(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (_state.IsInvalid) @@ -92,9 +95,14 @@ internal void SetWindow(int window) InitializeEncoder(); Debug.Assert(_state != null && !_state.IsInvalid && !_state.IsClosed); } - if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_Max) + if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_MaxLarge) + { + throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_MaxLarge)); + } + if (window > BrotliUtils.WindowBits_Max && + Interop.Brotli.BrotliEncoderSetParameter(_state, BrotliEncoderParameter.LargeWindow, 1) == Interop.BOOL.FALSE) { - throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_Max)); + throw new InvalidOperationException(SR.Format(SR.BrotliEncoder_InvalidSetParameter, nameof(BrotliEncoderParameter.LargeWindow))); } if (Interop.Brotli.BrotliEncoderSetParameter(_state, BrotliEncoderParameter.LGWin, (uint)window) == Interop.BOOL.FALSE) { @@ -196,7 +204,7 @@ internal OperationStatus Compress(ReadOnlySpan source, Span destinat /// 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 . /// A number representing quality of the Brotli compression. 0 is the minimum (no compression), 11 is the maximum. - /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 24. + /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 30. /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int window) { @@ -204,11 +212,13 @@ public static bool TryCompress(ReadOnlySpan source, Span destination { throw new ArgumentOutOfRangeException(nameof(quality), SR.Format(SR.BrotliEncoder_Quality, quality, 0, BrotliUtils.Quality_Max)); } - if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_Max) + if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_MaxLarge) { - throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_Max)); + throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_MaxLarge)); } + BrotliUtils.ValidateLargeWindowQuality(quality, window); + unsafe { fixed (byte* inBytes = &MemoryMarshal.GetReference(source)) diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs index b7cfa8b93794a0..3de5b5d31d5b24 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs @@ -10,6 +10,7 @@ namespace System.IO.Compression /// LGBlock - Recommended input block size. Encoder may reduce this value, e.g. if input is much smaller than window size. Range is from BROTLI_MIN_INPUT_BLOCK_BITS to BROTLI_MAX_INPUT_BLOCK_BITS. Bigger input block size allows better compression, but consumes more memory. /// LCModeling- Flag that affects usage of "literal context modeling" format feature. This flag is a "decoding-speed vs compression ratio" trade-off. /// SizeHint - Estimated total input size for all BrotliEncoderCompressStream calls. The default value is 0, which means that the total input size is unknown. + /// LargeWindow - Flag that determines if "Large Window Brotli" is used, allowing LGWin to range up to BROTLI_LARGE_MAX_WINDOW_BITS instead of BROTLI_MAX_WINDOW_BITS. /// internal enum BrotliEncoderParameter { @@ -18,6 +19,7 @@ internal enum BrotliEncoderParameter LGWin, LGBlock, LCModeling, - SizeHint + SizeHint, + LargeWindow } } diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs index 50cd37b0b65d3f..f488a9c41307cf 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs @@ -36,10 +36,13 @@ public BrotliStream(Stream stream, CompressionLevel compressionLevel, bool leave /// The Brotli options for fine tuning the compression stream. /// to leave the stream open after disposing the object; otherwise, . /// or is . + /// is greater than 24, which requires the "Large Window Brotli" extension, but is too low for the encoder to use it. public BrotliStream(Stream stream, BrotliCompressionOptions compressionOptions, bool leaveOpen = false) : this(stream, CompressionMode.Compress, leaveOpen) { ArgumentNullException.ThrowIfNull(compressionOptions); + BrotliUtils.ValidateLargeWindowQuality(compressionOptions.Quality, compressionOptions.WindowLog2); + _encoder.SetQuality(compressionOptions.Quality); _encoder.SetWindow(compressionOptions.WindowLog2); } diff --git a/src/native/libs/System.IO.Compression.Native/entrypoints.c b/src/native/libs/System.IO.Compression.Native/entrypoints.c index 4b72f5d3c0fb35..78a4e9953984e7 100644 --- a/src/native/libs/System.IO.Compression.Native/entrypoints.c +++ b/src/native/libs/System.IO.Compression.Native/entrypoints.c @@ -25,6 +25,7 @@ static const Entry s_compressionNative[] = DllImportEntry(BrotliDecoderDecompressStream) DllImportEntry(BrotliDecoderDestroyInstance) DllImportEntry(BrotliDecoderIsFinished) + DllImportEntry(BrotliDecoderSetParameter) DllImportEntry(BrotliEncoderCompress) DllImportEntry(BrotliEncoderCompressStream) DllImportEntry(BrotliEncoderCreateInstance) From a447b47729b063dc97e81276de6b5c2a3c6f55ed Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Oct 2026 10:03:15 +0200 Subject: [PATCH 02/11] Change the return of declined data --- .../System.IO.Compression.Brotli/src/Resources/Strings.resx | 3 --- .../src/System/IO/Compression/dec/BrotliDecoder.cs | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx index c1e91bb374be2d..222da31e61db72 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx @@ -161,9 +161,6 @@ The BrotliDecoder {0} can not be changed at current decoder state. - - The Brotli stream requires a window size of 2^{0} bytes, which is greater than the maxWindowLog2 of 2^{1} bytes allowed by the decompression options. - Can not perform Read operations on a BrotliStream constructed with CompressionMode.Compress. diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs index ac15c31f3875db..96e823f227c333 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs @@ -80,12 +80,11 @@ private void EnsureNotDisposed() /// The total number of bytes that were read from . /// The total number of bytes that were written in the . /// One of the enumeration values that indicates the status of the decompression operation. - /// The window size declared by is greater than the configured maximum. /// The return value can be as follows: /// - : was successfully and completely decompressed into . /// - : There is not enough space in to decompress . /// - : The decompression action is partially done at least one more byte is required to complete the decompression task. This method should be called again with more input to decompress. - /// - : The data in is invalid and could not be decompressed. + /// - : The data in is invalid and could not be decompressed, or the window size it declares is greater than the configured maximum. public OperationStatus Decompress(ReadOnlySpan source, Span destination, out int bytesConsumed, out int bytesWritten) { EnsureInitialized(); @@ -102,7 +101,7 @@ public OperationStatus Decompress(ReadOnlySpan source, Span destinat return OperationStatus.NeedMoreData; case OperationStatus.Done when windowBits > _maxWindowLog2: - throw new IOException(SR.Format(SR.BrotliDecoder_MaxWindowLog2Exceeded, windowBits, _maxWindowLog2)); + return OperationStatus.InvalidData; default: // A malformed header is reported by the native decoder, so that the error matches what callers that did not From abd7c44f40c7f7db56eea963d301249349757094 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Oct 2026 10:03:28 +0200 Subject: [PATCH 03/11] Add tests --- .../tests/BrotliEncoderDecoderTests.cs | 2 +- .../CompressionStreamUnitTests.Brotli.cs | 150 +++++++++++++++++- 2 files changed, 148 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs b/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs index 49f5adfb331cf6..f33ef237a0d7ad 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs @@ -25,7 +25,7 @@ public class BrotliEncoderDecoderTests : EncoderDecoderTestBase protected override int InvalidQualityTooLow => -1; protected override int InvalidQualityTooHigh => 12; protected override int InvalidWindowLogTooLow => 9; - protected override int InvalidWindowLogTooHigh => 25; + protected override int InvalidWindowLogTooHigh => 31; public class BrotliEncoderAdapter : EncoderAdapter { diff --git a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs index f69aae87bf661b..12b3e705fb7707 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs @@ -60,10 +60,10 @@ public void InvalidWindow() { Assert.Throws("window", () => new BrotliEncoder(10, -1)); Assert.Throws("window", () => new BrotliEncoder(10, 9)); - Assert.Throws("window", () => new BrotliEncoder(10, 25)); + Assert.Throws("window", () => new BrotliEncoder(10, 31)); Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, -1)); Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, 9)); - Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, 25)); + Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, 31)); } [Fact] @@ -107,7 +107,7 @@ public void InvalidBrotliCompressionWindowLog2() Assert.Equal(22, options.WindowLog2); // default value Assert.Throws("value", () => options.WindowLog2 = -1); Assert.Throws("value", () => options.WindowLog2 = 9); - Assert.Throws("value", () => options.WindowLog2 = 25); + Assert.Throws("value", () => options.WindowLog2 = 31); } [Theory] @@ -482,6 +482,150 @@ private static void Decompress_WithoutState(ReadOnlySpan input, Span BrotliDecoder.TryDecompress(input, output, out int bytesWritten); } + [Fact] + public void InvalidBrotliDecompressionMaxWindowLog2() + { + BrotliDecompressionOptions options = new(); + + Assert.Equal(24, options.MaxWindowLog2); // default value + Assert.Throws("value", () => options.MaxWindowLog2 = -1); + Assert.Throws("value", () => options.MaxWindowLog2 = 9); + Assert.Throws("value", () => options.MaxWindowLog2 = 31); + } + + [Fact] + public void BrotliDecompressionOptions_NullArgument_Throws() + { + Assert.Throws("decompressionOptions", () => new BrotliDecoder(null)); + Assert.Throws("decompressionOptions", () => new BrotliStream(new MemoryStream(), (BrotliDecompressionOptions)null)); + Assert.Throws("stream", () => new BrotliStream(null, new BrotliDecompressionOptions())); + } + + [Theory] + [InlineData(10)] + [InlineData(16)] + [InlineData(17)] + [InlineData(22)] + [InlineData(24)] + public void BrotliDecompressionOptions_StandardWindow_RoundTrips(int windowLog2) + { + byte[] testData = CreateWindowTestData(); + byte[] compressed = CompressWithWindowLog2(testData, windowLog2); + + Assert.Equal(testData, DecompressWithOptions(compressed, new BrotliDecompressionOptions { MaxWindowLog2 = windowLog2 })); + } + + [Theory] + [InlineData(25)] + [InlineData(30)] + public void BrotliLargeWindow_RequiresOptIn(int windowLog2) + { + byte[] testData = CreateWindowTestData(); + byte[] compressed = CompressWithWindowLog2(testData, windowLog2); + + // Large Window Brotli is not part of RFC 7932, so a decoder that did not opt in rejects the stream. + Assert.Throws(() => DecompressWithOptions(compressed, decompressionOptions: null)); + + Assert.Equal(testData, DecompressWithOptions(compressed, new BrotliDecompressionOptions { MaxWindowLog2 = windowLog2 })); + + // A window larger than the configured maximum is reported the same way as any other invalid stream. + Assert.Throws(() => DecompressWithOptions(compressed, new BrotliDecompressionOptions { MaxWindowLog2 = windowLog2 - 1 })); + } + + [Fact] + public void BrotliDecompressionOptions_RejectsWindowLargerThanMaxWindowLog2() + { + byte[] testData = CreateWindowTestData(); + byte[] compressed = CompressWithWindowLog2(testData, 22); + + // A window larger than the configured maximum is reported the same way as any other invalid stream. + Assert.Throws(() => DecompressWithOptions(compressed, new BrotliDecompressionOptions { MaxWindowLog2 = 10 })); + + Assert.Equal(testData, DecompressWithOptions(compressed, new BrotliDecompressionOptions { MaxWindowLog2 = 22 })); + } + + [Fact] + public void BrotliDecoder_LargeWindow_RoundTrips() + { + byte[] testData = CreateWindowTestData(); + byte[] compressed = CompressWithWindowLog2(testData, 30); + byte[] decompressed = new byte[testData.Length]; + + using (BrotliDecoder decoder = new(new BrotliDecompressionOptions { MaxWindowLog2 = 30 })) + { + Assert.Equal(OperationStatus.Done, decoder.Decompress(compressed, decompressed, out int bytesConsumed, out int bytesWritten)); + Assert.Equal(compressed.Length, bytesConsumed); + Assert.Equal(testData.Length, bytesWritten); + } + + Assert.Equal(testData, decompressed); + + BrotliDecompressionOptions limitedOptions = new() { MaxWindowLog2 = 29 }; + using BrotliDecoder limitedDecoder = new(limitedOptions); + Assert.Equal(OperationStatus.InvalidData, limitedDecoder.Decompress(compressed, new byte[testData.Length], out int limitedBytesConsumed, out int limitedBytesWritten)); + Assert.Equal(0, limitedBytesConsumed); + Assert.Equal(0, limitedBytesWritten); + } + + [Fact] + public void BrotliDecoder_LargeWindowHeaderSplitAcrossCalls_ReportsNeedMoreData() + { + byte[] compressed = CompressWithWindowLog2(CreateWindowTestData(), 30); + + using BrotliDecoder decoder = new(new BrotliDecompressionOptions { MaxWindowLog2 = 30 }); + + // The Large Window Brotli header spans two bytes, so one byte is not enough to determine the window size. + Assert.Equal(OperationStatus.NeedMoreData, decoder.Decompress(compressed.AsSpan(0, 1), new byte[64], out int bytesConsumed, out int bytesWritten)); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData(0, 25)] + [InlineData(2, 30)] + public void BrotliLargeWindow_QualityTooLow_Throws(int quality, int windowLog2) + { + Assert.Throws(() => new BrotliEncoder(quality, windowLog2)); + Assert.Throws(() => new BrotliStream(new MemoryStream(), new BrotliCompressionOptions { Quality = quality, WindowLog2 = windowLog2 })); + Assert.Throws(() => BrotliEncoder.TryCompress(new byte[1], new byte[64], out _, quality, windowLog2)); + } + + private static byte[] CreateWindowTestData() + { + // Deliberately smaller than the encoder input block size, so that declaring a large window does not make either + // side allocate a buffer proportional to that window. + byte[] data = new byte[8 * 1024]; + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)(i * 31); + } + + return data; + } + + private static byte[] CompressWithWindowLog2(byte[] input, int windowLog2) + { + using MemoryStream output = new(); + using (BrotliStream compressor = new(output, new BrotliCompressionOptions { WindowLog2 = windowLog2 }, leaveOpen: true)) + { + compressor.Write(input); + } + + return output.ToArray(); + } + + private static byte[] DecompressWithOptions(byte[] compressed, BrotliDecompressionOptions decompressionOptions) + { + using MemoryStream input = new(compressed); + using BrotliStream decompressor = decompressionOptions is null + ? new BrotliStream(input, CompressionMode.Decompress) + : new BrotliStream(input, decompressionOptions); + using MemoryStream output = new(); + + decompressor.CopyTo(output); + return output.ToArray(); + } + private static MemoryStream Compress_Stream(ReadOnlySpan input, CompressionLevel compressionLevel) { using (var inputStream = new MemoryStream(input.ToArray())) From 9b32609cd2e2aecafe3989059f3f47b7f2e56888 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Oct 2026 10:56:19 +0200 Subject: [PATCH 04/11] Adding BrotliDecoderSetParameter native method --- .../System.IO.Compression.Native.def | 1 + .../System.IO.Compression.Native_unixexports.src | 1 + 2 files changed, 2 insertions(+) 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 34496cc73b6bad..b6b6eb0537b6ca 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 @@ -13,6 +13,7 @@ EXPORTS BrotliEncoderHasMoreOutput BrotliEncoderMaxCompressedSize BrotliEncoderSetParameter + BrotliDecoderSetParameter CompressionNative_Crc32 CompressionNative_Deflate CompressionNative_DeflateEnd 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 fbb9536b99cc82..9fc99c54a99334 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 @@ -13,6 +13,7 @@ BrotliEncoderDestroyInstance BrotliEncoderHasMoreOutput BrotliEncoderMaxCompressedSize BrotliEncoderSetParameter +BrotliDecoderSetParameter CompressionNative_Crc32 CompressionNative_Deflate CompressionNative_DeflateEnd From c1c8907031a95f986765b7e9989605fadb8a5ab0 Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Oct 2026 16:33:13 +0200 Subject: [PATCH 05/11] Revert all changes to encoder side --- .../src/Resources/Strings.resx | 3 - .../src/System/IO/Compression/BrotliUtils.cs | 32 +++---- .../enc/BrotliCompressionOptions.cs | 7 +- .../IO/Compression/enc/BrotliEncoder.cs | 24 ++--- .../Compression/enc/BrotliEncoderParameter.cs | 4 +- .../Compression/enc/BrotliStream.Compress.cs | 3 - .../CompressionStreamUnitTests.Brotli.cs | 89 +++++++++++++++---- 7 files changed, 95 insertions(+), 67 deletions(-) diff --git a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx index 222da31e61db72..7b49fbd3592f7b 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx @@ -148,9 +148,6 @@ The BrotliEncoder {0} can not be changed at current encoder state. - - A window of {0} requires the Large Window Brotli extension, which the encoder does not use at the specified quality of {1}. Specify a quality greater than {2}. - Failed to create BrotliDecoder instance diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs index ee5effd292ecca..7f6d05029e0996 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs @@ -15,26 +15,26 @@ internal static partial class BrotliUtils public const int Quality_Min = 0; public const int Quality_Default = 4; public const int Quality_Max = 11; - /// - /// The highest quality that uses static entropy codes (MAX_QUALITY_FOR_STATIC_ENTROPY_CODES). The native encoder silently - /// disables large-window mode at or below this quality, clamping the window back to . - /// - public const int Quality_MaxForStaticEntropyCodes = 2; /// /// Reads the window size declared by the header of a Brotli stream without consuming any input. /// /// A buffer positioned at the start of a Brotli stream. /// When this method returns , the declared base-2 logarithm of the window size. + /// + /// if the decoder has "Large Window Brotli" enabled; otherwise, . + /// /// /// if the window size was determined; if /// is too short to determine it; otherwise . /// /// /// The header is read least-significant-bit first and occupies at most 14 bits. This mirrors DecodeWindowBits and the - /// BROTLI_STATE_LARGE_WINDOW_BITS state of the native decoder. + /// BROTLI_STATE_LARGE_WINDOW_BITS state of the native decoder: when is + /// , the "Large Window Brotli" escape is rejected as soon as it is recognized, without waiting + /// for the byte that would otherwise carry the window size, matching the native decoder's behavior. /// - internal static OperationStatus TryGetWindowBits(ReadOnlySpan source, out int windowBits) + internal static OperationStatus TryGetWindowBits(ReadOnlySpan source, out int windowBits, bool largeWindowEnabled) { // The header is a variable-length prefix, read least-significant-bit first: // bit 0 == 0 -> window is 16 @@ -74,6 +74,13 @@ internal static OperationStatus TryGetWindowBits(ReadOnlySpan source, out return OperationStatus.Done; } + // The native decoder rejects the escape outright when Large Window Brotli is disabled, without ever looking at + // the reserved bit or the window size that would otherwise follow. + if (!largeWindowEnabled) + { + return OperationStatus.InvalidData; + } + // The escape is followed by a reserved bit that must be zero, and then by the window size itself. if ((header & 0b1000_0000) != 0) { @@ -91,17 +98,6 @@ internal static OperationStatus TryGetWindowBits(ReadOnlySpan source, out : OperationStatus.Done; } - /// - /// Validates that a large window size is paired with a quality that the native encoder will honor. - /// - internal static void ValidateLargeWindowQuality(int quality, int windowLog2) - { - if (windowLog2 > WindowBits_Max && quality <= Quality_MaxForStaticEntropyCodes) - { - throw new ArgumentException(SR.Format(SR.BrotliEncoder_LargeWindowQuality, windowLog2, quality, Quality_MaxForStaticEntropyCodes)); - } - } - internal static int GetQualityFromCompressionLevel(CompressionLevel compressionLevel) => compressionLevel switch { diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs index 25dc772b581748..cb7f5322d05d1a 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliCompressionOptions.cs @@ -33,10 +33,9 @@ public int Quality /// /// Gets or sets the base-2 logarithm of the window size for a Brotli compression stream. /// - /// The value is less than 10 or greater than 30. + /// The value is less than 10 or greater than 24. /// - /// The value is expressed as the base-2 logarithm of the size in bytes of the sliding window used by the LZ77 algorithm. Larger window sizes can improve compression ratio but use more memory. Range is from 10 to 30. The default value is 22. - /// Values greater than 24 produce a stream that uses the "Large Window Brotli" extension, which is not part of RFC 7932 and which decoders must explicitly opt into. Such values also require a greater than 2. + /// The value is expressed as the base-2 logarithm of the size in bytes of the sliding window used by the LZ77 algorithm. Larger window sizes can improve compression ratio but use more memory. Range is from 10 to 24. The default value is 22. /// public int WindowLog2 { @@ -44,7 +43,7 @@ public int WindowLog2 set { ArgumentOutOfRangeException.ThrowIfLessThan(value, BrotliUtils.WindowBits_Min, nameof(value)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value, BrotliUtils.WindowBits_MaxLarge, nameof(value)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, BrotliUtils.WindowBits_Max, nameof(value)); _windowLog2 = value; } diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs index f5b85b404ae045..e2f2b74ae38c3d 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs @@ -16,16 +16,13 @@ public partial struct BrotliEncoder : IDisposable /// Initializes a new instance of the structure using the specified quality and window. /// A number representing quality of the Brotli compression. 0 is the minimum (no compression), 11 is the maximum. - /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 30. + /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 24. /// is not between the minimum value of 0 and the maximum value of 11. /// -or- - /// is not between the minimum value of 10 and the maximum value of 30. - /// is greater than 24, which requires the "Large Window Brotli" extension, but is too low for the encoder to use it. + /// is not between the minimum value of 10 and the maximum value of 24. /// Failed to create the instance. public BrotliEncoder(int quality, int window) { - BrotliUtils.ValidateLargeWindowQuality(quality, window); - _disposed = false; _state = Interop.Brotli.BrotliEncoderCreateInstance(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (_state.IsInvalid) @@ -95,14 +92,9 @@ internal void SetWindow(int window) InitializeEncoder(); Debug.Assert(_state != null && !_state.IsInvalid && !_state.IsClosed); } - if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_MaxLarge) - { - throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_MaxLarge)); - } - if (window > BrotliUtils.WindowBits_Max && - Interop.Brotli.BrotliEncoderSetParameter(_state, BrotliEncoderParameter.LargeWindow, 1) == Interop.BOOL.FALSE) + if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_Max) { - throw new InvalidOperationException(SR.Format(SR.BrotliEncoder_InvalidSetParameter, nameof(BrotliEncoderParameter.LargeWindow))); + throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_Max)); } if (Interop.Brotli.BrotliEncoderSetParameter(_state, BrotliEncoderParameter.LGWin, (uint)window) == Interop.BOOL.FALSE) { @@ -204,7 +196,7 @@ internal OperationStatus Compress(ReadOnlySpan source, Span destinat /// 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 . /// A number representing quality of the Brotli compression. 0 is the minimum (no compression), 11 is the maximum. - /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 30. + /// A number representing the encoder window bits. The minimum value is 10, and the maximum value is 24. /// if the compression operation was successful; otherwise. public static bool TryCompress(ReadOnlySpan source, Span destination, out int bytesWritten, int quality, int window) { @@ -212,13 +204,11 @@ public static bool TryCompress(ReadOnlySpan source, Span destination { throw new ArgumentOutOfRangeException(nameof(quality), SR.Format(SR.BrotliEncoder_Quality, quality, 0, BrotliUtils.Quality_Max)); } - if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_MaxLarge) + if (window < BrotliUtils.WindowBits_Min || window > BrotliUtils.WindowBits_Max) { - throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_MaxLarge)); + throw new ArgumentOutOfRangeException(nameof(window), SR.Format(SR.BrotliEncoder_Window, window, BrotliUtils.WindowBits_Min, BrotliUtils.WindowBits_Max)); } - BrotliUtils.ValidateLargeWindowQuality(quality, window); - unsafe { fixed (byte* inBytes = &MemoryMarshal.GetReference(source)) diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs index 3de5b5d31d5b24..b7cfa8b93794a0 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoderParameter.cs @@ -10,7 +10,6 @@ namespace System.IO.Compression /// LGBlock - Recommended input block size. Encoder may reduce this value, e.g. if input is much smaller than window size. Range is from BROTLI_MIN_INPUT_BLOCK_BITS to BROTLI_MAX_INPUT_BLOCK_BITS. Bigger input block size allows better compression, but consumes more memory. /// LCModeling- Flag that affects usage of "literal context modeling" format feature. This flag is a "decoding-speed vs compression ratio" trade-off. /// SizeHint - Estimated total input size for all BrotliEncoderCompressStream calls. The default value is 0, which means that the total input size is unknown. - /// LargeWindow - Flag that determines if "Large Window Brotli" is used, allowing LGWin to range up to BROTLI_LARGE_MAX_WINDOW_BITS instead of BROTLI_MAX_WINDOW_BITS. /// internal enum BrotliEncoderParameter { @@ -19,7 +18,6 @@ internal enum BrotliEncoderParameter LGWin, LGBlock, LCModeling, - SizeHint, - LargeWindow + SizeHint } } diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs index f488a9c41307cf..50cd37b0b65d3f 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs @@ -36,13 +36,10 @@ public BrotliStream(Stream stream, CompressionLevel compressionLevel, bool leave /// The Brotli options for fine tuning the compression stream. /// to leave the stream open after disposing the object; otherwise, . /// or is . - /// is greater than 24, which requires the "Large Window Brotli" extension, but is too low for the encoder to use it. public BrotliStream(Stream stream, BrotliCompressionOptions compressionOptions, bool leaveOpen = false) : this(stream, CompressionMode.Compress, leaveOpen) { ArgumentNullException.ThrowIfNull(compressionOptions); - BrotliUtils.ValidateLargeWindowQuality(compressionOptions.Quality, compressionOptions.WindowLog2); - _encoder.SetQuality(compressionOptions.Quality); _encoder.SetWindow(compressionOptions.WindowLog2); } diff --git a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs index 12b3e705fb7707..58480bdd362efa 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs @@ -520,8 +520,8 @@ public void BrotliDecompressionOptions_StandardWindow_RoundTrips(int windowLog2) [InlineData(30)] public void BrotliLargeWindow_RequiresOptIn(int windowLog2) { - byte[] testData = CreateWindowTestData(); - byte[] compressed = CompressWithWindowLog2(testData, windowLog2); + byte[] testData = s_largeWindowPayload; + byte[] compressed = CreateLargeWindowCompressed(windowLog2); // Large Window Brotli is not part of RFC 7932, so a decoder that did not opt in rejects the stream. Assert.Throws(() => DecompressWithOptions(compressed, decompressionOptions: null)); @@ -547,8 +547,8 @@ public void BrotliDecompressionOptions_RejectsWindowLargerThanMaxWindowLog2() [Fact] public void BrotliDecoder_LargeWindow_RoundTrips() { - byte[] testData = CreateWindowTestData(); - byte[] compressed = CompressWithWindowLog2(testData, 30); + byte[] testData = s_largeWindowPayload; + byte[] compressed = CreateLargeWindowCompressed(30); byte[] decompressed = new byte[testData.Length]; using (BrotliDecoder decoder = new(new BrotliDecompressionOptions { MaxWindowLog2 = 30 })) @@ -570,7 +570,7 @@ public void BrotliDecoder_LargeWindow_RoundTrips() [Fact] public void BrotliDecoder_LargeWindowHeaderSplitAcrossCalls_ReportsNeedMoreData() { - byte[] compressed = CompressWithWindowLog2(CreateWindowTestData(), 30); + byte[] compressed = CreateLargeWindowCompressed(30); using BrotliDecoder decoder = new(new BrotliDecompressionOptions { MaxWindowLog2 = 30 }); @@ -580,27 +580,43 @@ public void BrotliDecoder_LargeWindowHeaderSplitAcrossCalls_ReportsNeedMoreData( Assert.Equal(0, bytesWritten); } - [Theory] - [InlineData(0, 25)] - [InlineData(2, 30)] - public void BrotliLargeWindow_QualityTooLow_Throws(int quality, int windowLog2) + [Fact] + public void BrotliDecoder_EmptyDestination_DoesNotBypassWindowValidation() { - Assert.Throws(() => new BrotliEncoder(quality, windowLog2)); - Assert.Throws(() => new BrotliStream(new MemoryStream(), new BrotliCompressionOptions { Quality = quality, WindowLog2 = windowLog2 })); - Assert.Throws(() => BrotliEncoder.TryCompress(new byte[1], new byte[64], out _, quality, windowLog2)); + byte[] testData = s_largeWindowPayload; + byte[] largeWindowCompressed = CreateLargeWindowCompressed(30); + + using BrotliDecoder decoder = new(new BrotliDecompressionOptions { MaxWindowLog2 = 25 }); + + // An empty destination makes no decompression progress, so the source below is never actually consumed. + byte[] unconsumedSource = { 0x00 }; + Assert.Equal(OperationStatus.DestinationTooSmall, decoder.Decompress(unconsumedSource, Array.Empty(), out int bytesConsumed, out int bytesWritten)); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + + // Swapping in a stream whose window (30) exceeds the configured maximum (25) must still be rejected, even + // though the previous call saw a header that would have been within range. + Assert.Equal(OperationStatus.InvalidData, decoder.Decompress(largeWindowCompressed, new byte[testData.Length], out bytesConsumed, out bytesWritten)); + Assert.Equal(0, bytesConsumed); + Assert.Equal(0, bytesWritten); + } + + [Fact] + public void BrotliStream_SingleByteLargeWindowEscape_ThrowsForBothConstructors() + { + // 0x11 starts the "Large Window Brotli" escape sequence. Without opting in, the native decoder rejects it + // immediately rather than waiting for the second header byte. + byte[] truncatedLargeWindowEscape = { 0x11 }; + + Assert.Throws(() => Decompress_Stream(truncatedLargeWindowEscape)); + Assert.Throws(() => DecompressWithOptions(truncatedLargeWindowEscape, new BrotliDecompressionOptions())); } private static byte[] CreateWindowTestData() { // Deliberately smaller than the encoder input block size, so that declaring a large window does not make either // side allocate a buffer proportional to that window. - byte[] data = new byte[8 * 1024]; - for (int i = 0; i < data.Length; i++) - { - data[i] = (byte)(i * 31); - } - - return data; + return CreatePattern(8 * 1024); } private static byte[] CompressWithWindowLog2(byte[] input, int windowLog2) @@ -614,6 +630,41 @@ private static byte[] CompressWithWindowLog2(byte[] input, int windowLog2) return output.ToArray(); } + // BrotliEncoder only ever produces streams up to the standard 24-bit window (RFC 7932); it has no "Large Window + // Brotli" support, and this test suite intentionally keeps it that way (see PR discussion on #135846). This + // fixture was generated once, offline, by driving the native encoder directly (BrotliEncoderCompressStream with + // BROTLI_PARAM_LARGE_WINDOW=1) to produce a genuine "Large Window Brotli" stream for s_largeWindowPayload. + // Only byte[1] (the declared window value, 10-30, encoded in the low 6 bits) varies between window sizes; the + // metablock payload for this tiny input is identical regardless of the configured window. + private static readonly byte[] s_largeWindowPayload = CreatePattern(64); + + private static readonly byte[] s_largeWindowFixtureTemplate = + { + 0x11, 0x1E, 0x7E, 0x00, 0x02, 0x00, 0x1F, 0x3E, 0x5D, 0x7C, 0x9B, 0xBA, 0xD9, 0xF8, 0x17, 0x36, + 0x55, 0x74, 0x93, 0xB2, 0xD1, 0xF0, 0x0F, 0x2E, 0x4D, 0x6C, 0x8B, 0xAA, 0xC9, 0xE8, 0x07, 0x26, + 0x45, 0x64, 0x83, 0xA2, 0xC1, 0xE0, 0xFF, 0x1E, 0x3D, 0x5C, 0x7B, 0x9A, 0xB9, 0xD8, 0xF7, 0x16, + 0x35, 0x54, 0x73, 0x92, 0xB1, 0xD0, 0xEF, 0x0E, 0x2D, 0x4C, 0x6B, 0x8A, 0xA9, 0xC8, 0xE7, 0x06, + 0x25, 0x44, 0x63, 0x82, 0xA1, 0x03 + }; + + private static byte[] CreatePattern(int length) + { + byte[] data = new byte[length]; + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)(i * 31); + } + + return data; + } + + private static byte[] CreateLargeWindowCompressed(int windowLog2) + { + byte[] compressed = (byte[])s_largeWindowFixtureTemplate.Clone(); + compressed[1] = (byte)windowLog2; + return compressed; + } + private static byte[] DecompressWithOptions(byte[] compressed, BrotliDecompressionOptions decompressionOptions) { using MemoryStream input = new(compressed); From f71a6dae61e07ceafa19af04b6d9ee34c36120df Mon Sep 17 00:00:00 2001 From: iremyux Date: Thu, 15 Oct 2026 16:33:27 +0200 Subject: [PATCH 06/11] Review changes --- .../src/System/IO/Compression/dec/BrotliDecoder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs index 96e823f227c333..1b1972dd2d64d1 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs @@ -93,9 +93,12 @@ public OperationStatus Decompress(ReadOnlySpan source, Span destinat bytesConsumed = 0; bytesWritten = 0; - if (_maxWindowLog2 != 0 && !_windowValidated) + // An empty destination makes no decompression progress, so no input is actually consumed below; deferring + // validation until a call that can make progress prevents the caller from swapping out the unconsumed input + // for a stream with a different window size and bypassing the check entirely. + if (_maxWindowLog2 != 0 && !_windowValidated && !destination.IsEmpty) { - switch (BrotliUtils.TryGetWindowBits(source, out int windowBits)) + switch (BrotliUtils.TryGetWindowBits(source, out int windowBits, EffectiveMaxWindowLog2 > BrotliUtils.WindowBits_Max)) { case OperationStatus.NeedMoreData: return OperationStatus.NeedMoreData; From 7f36c3db12fb4ee2d0842ae304213de6c32e8770 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 16 Oct 2026 14:24:41 +0200 Subject: [PATCH 07/11] Fix tests after encoder revert --- .../tests/BrotliEncoderDecoderTests.cs | 2 +- .../tests/CompressionStreamUnitTests.Brotli.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs b/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs index f33ef237a0d7ad..49f5adfb331cf6 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/BrotliEncoderDecoderTests.cs @@ -25,7 +25,7 @@ public class BrotliEncoderDecoderTests : EncoderDecoderTestBase protected override int InvalidQualityTooLow => -1; protected override int InvalidQualityTooHigh => 12; protected override int InvalidWindowLogTooLow => 9; - protected override int InvalidWindowLogTooHigh => 31; + protected override int InvalidWindowLogTooHigh => 25; public class BrotliEncoderAdapter : EncoderAdapter { diff --git a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs index 58480bdd362efa..14b9abbd601b71 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs @@ -60,10 +60,10 @@ public void InvalidWindow() { Assert.Throws("window", () => new BrotliEncoder(10, -1)); Assert.Throws("window", () => new BrotliEncoder(10, 9)); - Assert.Throws("window", () => new BrotliEncoder(10, 31)); + Assert.Throws("window", () => new BrotliEncoder(10, 25)); Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, -1)); Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, 9)); - Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, 31)); + Assert.Throws("window", () => BrotliEncoder.TryCompress(new ReadOnlySpan(), new Span(), out int bytesWritten, 6, 25)); } [Fact] @@ -107,7 +107,7 @@ public void InvalidBrotliCompressionWindowLog2() Assert.Equal(22, options.WindowLog2); // default value Assert.Throws("value", () => options.WindowLog2 = -1); Assert.Throws("value", () => options.WindowLog2 = 9); - Assert.Throws("value", () => options.WindowLog2 = 31); + Assert.Throws("value", () => options.WindowLog2 = 25); } [Theory] From 2691487188d7fb8b64714d19fae714f87e4dc5a5 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 19 Oct 2026 20:29:18 +0200 Subject: [PATCH 08/11] Removed EffectiveMaxWindowLog2 --- .../src/System/IO/Compression/dec/BrotliDecoder.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs index 1b1972dd2d64d1..9870697bd841e6 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs @@ -15,12 +15,6 @@ public struct BrotliDecoder : IDisposable private bool _disposed; private int _maxWindowLog2; private bool _windowValidated; - /// - /// Gets the maximum window size accepted by this decoder. A zero-initialized has no explicitly - /// configured value and defaults to the maximum defined by RFC 7932. - /// - private readonly int EffectiveMaxWindowLog2 => _maxWindowLog2 == 0 ? BrotliUtils.WindowBits_Max : _maxWindowLog2; - /// Initializes a new instance of the structure using the specified decompression options. /// The Brotli options for fine tuning the decompression. @@ -47,7 +41,7 @@ internal void InitializeDecoder() // The native decoder only distinguishes between the RFC 7932 window range and the "Large Window Brotli" range; any // finer limit is enforced against the window declared by the stream header once decompression starts. - if (EffectiveMaxWindowLog2 > BrotliUtils.WindowBits_Max && + if (_maxWindowLog2 > BrotliUtils.WindowBits_Max && Interop.Brotli.BrotliDecoderSetParameter(_state, BrotliDecoderParameter.LargeWindow, 1) == Interop.BOOL.FALSE) { throw new InvalidOperationException(SR.Format(SR.BrotliDecoder_InvalidSetParameter, nameof(BrotliDecoderParameter.LargeWindow))); @@ -98,7 +92,7 @@ public OperationStatus Decompress(ReadOnlySpan source, Span destinat // for a stream with a different window size and bypassing the check entirely. if (_maxWindowLog2 != 0 && !_windowValidated && !destination.IsEmpty) { - switch (BrotliUtils.TryGetWindowBits(source, out int windowBits, EffectiveMaxWindowLog2 > BrotliUtils.WindowBits_Max)) + switch (BrotliUtils.TryGetWindowBits(source, out int windowBits, _maxWindowLog2 > BrotliUtils.WindowBits_Max)) { case OperationStatus.NeedMoreData: return OperationStatus.NeedMoreData; From 5f954f556876a57dc29f0f1b9f877be9839c4989 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 20 Oct 2026 13:39:45 +0200 Subject: [PATCH 09/11] Change exception type --- .../src/System/IO/Compression/dec/BrotliDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs index 9870697bd841e6..a22fd3b58be2c9 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs @@ -44,7 +44,7 @@ internal void InitializeDecoder() if (_maxWindowLog2 > BrotliUtils.WindowBits_Max && Interop.Brotli.BrotliDecoderSetParameter(_state, BrotliDecoderParameter.LargeWindow, 1) == Interop.BOOL.FALSE) { - throw new InvalidOperationException(SR.Format(SR.BrotliDecoder_InvalidSetParameter, nameof(BrotliDecoderParameter.LargeWindow))); + throw new IOException(SR.Format(SR.BrotliDecoder_InvalidSetParameter, nameof(BrotliDecoderParameter.LargeWindow))); } } From 0eed1fd8f37adadc0d3d230cc81971f9fe7b8a72 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 20 Oct 2026 13:53:36 +0200 Subject: [PATCH 10/11] Add WindowBits_DefaultLarge --- .../src/System/IO/Compression/BrotliUtils.cs | 5 +++++ .../System/IO/Compression/dec/BrotliDecompressionOptions.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs index 7f6d05029e0996..532dc6b474dfc7 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliUtils.cs @@ -12,6 +12,11 @@ internal static partial class BrotliUtils public const int WindowBits_Max = 24; /// The maximum window size supported by the "Large Window Brotli" extension (BROTLI_LARGE_MAX_WINDOW_BITS). public const int WindowBits_MaxLarge = 30; + /// + /// The default value of : the RFC 7932 ceiling, so decompression + /// behaves as it did before "Large Window Brotli" support was added unless a caller opts into a higher value. + /// + public const int WindowBits_DefaultLarge = WindowBits_Max; public const int Quality_Min = 0; public const int Quality_Default = 4; public const int Quality_Max = 11; diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs index 3b6ff9992646aa..063d686493d931 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecompressionOptions.cs @@ -8,7 +8,7 @@ namespace System.IO.Compression /// public sealed class BrotliDecompressionOptions { - private int _maxWindowLog2 = BrotliUtils.WindowBits_Max; + private int _maxWindowLog2 = BrotliUtils.WindowBits_DefaultLarge; /// /// Gets or sets the maximum base-2 logarithm of the window size accepted when decompressing. From ad0fa143781e39ca23898523c58cf772fa704f93 Mon Sep 17 00:00:00 2001 From: iremyux Date: Tue, 20 Oct 2026 18:23:14 +0200 Subject: [PATCH 11/11] Extend large-window split-header test to verify resuming after NeedMoreData --- .../CompressionStreamUnitTests.Brotli.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs index 14b9abbd601b71..3c9ee4291f88e9 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs @@ -570,6 +570,7 @@ public void BrotliDecoder_LargeWindow_RoundTrips() [Fact] public void BrotliDecoder_LargeWindowHeaderSplitAcrossCalls_ReportsNeedMoreData() { + byte[] testData = s_largeWindowPayload; byte[] compressed = CreateLargeWindowCompressed(30); using BrotliDecoder decoder = new(new BrotliDecompressionOptions { MaxWindowLog2 = 30 }); @@ -578,6 +579,27 @@ public void BrotliDecoder_LargeWindowHeaderSplitAcrossCalls_ReportsNeedMoreData( Assert.Equal(OperationStatus.NeedMoreData, decoder.Decompress(compressed.AsSpan(0, 1), new byte[64], out int bytesConsumed, out int bytesWritten)); Assert.Equal(0, bytesConsumed); Assert.Equal(0, bytesWritten); + + // Resubmitting the unconsumed header alongside the rest of the stream, through a small nonempty destination + // that forces repeated DestinationTooSmall calls, must still complete and retain the validated window. + byte[] decompressed = new byte[testData.Length]; + int totalConsumed = 0, totalWritten = 0; + ReadOnlySpan remaining = compressed; + OperationStatus status; + + do + { + int chunkSize = Math.Min(4, decompressed.Length - totalWritten); + status = decoder.Decompress(remaining, decompressed.AsSpan(totalWritten, chunkSize), out bytesConsumed, out bytesWritten); + remaining = remaining.Slice(bytesConsumed); + totalConsumed += bytesConsumed; + totalWritten += bytesWritten; + } while (status == OperationStatus.DestinationTooSmall); + + Assert.Equal(OperationStatus.Done, status); + Assert.Equal(compressed.Length, totalConsumed); + Assert.Equal(testData.Length, totalWritten); + Assert.Equal(testData, decompressed); } [Fact]