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..7b49fbd3592f7b 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx @@ -155,6 +155,9 @@ Can not access a closed Decoder. + + The BrotliDecoder {0} can not be changed at current decoder state. + 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..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 @@ -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,10 +10,99 @@ 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; + /// + /// 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; + /// + /// 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: 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, bool largeWindowEnabled) + { + // 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 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) + { + 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; + } + 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..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 @@ -13,12 +13,39 @@ public struct BrotliDecoder : IDisposable { private SafeBrotliDecoderHandle? _state; private bool _disposed; + private int _maxWindowLog2; + private bool _windowValidated; + + /// 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 (_maxWindowLog2 > BrotliUtils.WindowBits_Max && + Interop.Brotli.BrotliDecoderSetParameter(_state, BrotliDecoderParameter.LargeWindow, 1) == Interop.BOOL.FALSE) + { + throw new IOException(SR.Format(SR.BrotliDecoder_InvalidSetParameter, nameof(BrotliDecoderParameter.LargeWindow))); + } } internal void EnsureInitialized() @@ -51,7 +78,7 @@ private void EnsureNotDisposed() /// - : 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(); @@ -59,6 +86,28 @@ public OperationStatus Decompress(ReadOnlySpan source, Span destinat bytesConsumed = 0; bytesWritten = 0; + + // 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, _maxWindowLog2 > BrotliUtils.WindowBits_Max)) + { + case OperationStatus.NeedMoreData: + return OperationStatus.NeedMoreData; + + case OperationStatus.Done when 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 + // 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..063d686493d931 --- /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_DefaultLarge; + + /// + /// 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/tests/CompressionStreamUnitTests.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs index f69aae87bf661b..3c9ee4291f88e9 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/tests/CompressionStreamUnitTests.Brotli.cs @@ -482,6 +482,223 @@ 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 = 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)); + + 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 = s_largeWindowPayload; + byte[] compressed = CreateLargeWindowCompressed(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[] testData = s_largeWindowPayload; + byte[] compressed = CreateLargeWindowCompressed(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); + + // 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] + public void BrotliDecoder_EmptyDestination_DoesNotBypassWindowValidation() + { + 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. + return CreatePattern(8 * 1024); + } + + 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(); + } + + // 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); + 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())) 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 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)