Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/libraries/Common/src/Interop/Interop.Brotli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> source, System.Span<byte> destination, out int bytesConsumed, out int bytesWritten) { throw null; }
public void Dispose() { }
public static bool TryDecompress(System.ReadOnlySpan<byte> source, System.Span<byte> 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;
Expand All @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@
<data name="BrotliDecoder_Disposed" xml:space="preserve">
<value>Can not access a closed Decoder.</value>
</data>
<data name="BrotliDecoder_InvalidSetParameter" xml:space="preserve">
<value>The BrotliDecoder {0} can not be changed at current decoder state.</value>
</data>
<!-- BrotliStream.Compress -->
<data name="BrotliStream_Compress_UnsupportedOperation" xml:space="preserve">
<value>Can not perform Read operations on a BrotliStream constructed with CompressionMode.Compress.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<Compile Include="System\IO\Compression\enc\BrotliCompressionOptions.cs" />
<Compile Include="System\IO\Compression\enc\BrotliStream.Compress.cs" />
<Compile Include="System\IO\Compression\dec\BrotliStream.Decompress.cs" />
<Compile Include="System\IO\Compression\dec\BrotliDecompressionOptions.cs" />
<Compile Include="System\IO\Compression\dec\BrotliDecoderParameter.cs" />
<Compile Include="System\IO\Compression\BrotliUtils.cs" />
<Compile Include="System\IO\Compression\dec\BrotliDecoder.cs" />
<Compile Include="System\IO\Compression\enc\BrotliEncoder.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,108 @@
// 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
{
public const int WindowBits_Min = 10;
public const int WindowBits_Default = 22;
public const int WindowBits_Max = 24;
/// <summary>The maximum window size supported by the "Large Window Brotli" extension (BROTLI_LARGE_MAX_WINDOW_BITS).</summary>
public const int WindowBits_MaxLarge = 30;
/// <summary>
/// The default value of <see cref="BrotliDecompressionOptions.MaxWindowLog2"/>: 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.
/// </summary>
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;

/// <summary>
/// Reads the window size declared by the header of a Brotli stream without consuming any input.
/// </summary>
/// <param name="source">A buffer positioned at the start of a Brotli stream.</param>
/// <param name="windowBits">When this method returns <see cref="OperationStatus.Done"/>, the declared base-2 logarithm of the window size.</param>
/// <param name="largeWindowEnabled">
/// <see langword="true"/> if the decoder has "Large Window Brotli" enabled; otherwise, <see langword="false"/>.
/// </param>
/// <returns>
/// <see cref="OperationStatus.Done"/> if the window size was determined; <see cref="OperationStatus.NeedMoreData"/> if
/// <paramref name="source"/> is too short to determine it; otherwise <see cref="OperationStatus.InvalidData"/>.
/// </returns>
/// <remarks>
/// 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 <paramref name="largeWindowEnabled"/> is
/// <see langword="false"/>, 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.
/// </remarks>
internal static OperationStatus TryGetWindowBits(ReadOnlySpan<byte> 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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,39 @@ public struct BrotliDecoder : IDisposable
{
private SafeBrotliDecoderHandle? _state;
private bool _disposed;
private int _maxWindowLog2;
private bool _windowValidated;

/// <summary>Initializes a new instance of the <see cref="BrotliDecoder" /> structure using the specified decompression options.</summary>
/// <param name="decompressionOptions">The Brotli options for fine tuning the decompression.</param>
/// <exception cref="ArgumentNullException"><paramref name="decompressionOptions" /> is <see langword="null" />.</exception>
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)));
}
Comment thread
iremyux marked this conversation as resolved.
}

internal void EnsureInitialized()
Expand Down Expand Up @@ -51,14 +78,36 @@ private void EnsureNotDisposed()
/// - <see cref="System.Buffers.OperationStatus.Done" />: <paramref name="source" /> was successfully and completely decompressed into <paramref name="destination" />.
/// - <see cref="System.Buffers.OperationStatus.DestinationTooSmall" />: There is not enough space in <paramref name="destination" /> to decompress <paramref name="source" />.
/// - <see cref="System.Buffers.OperationStatus.NeedMoreData" />: 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.
/// - <see cref="System.Buffers.OperationStatus.InvalidData" />: The data in <paramref name="source" /> is invalid and could not be decompressed.</remarks>
/// - <see cref="System.Buffers.OperationStatus.InvalidData" />: The data in <paramref name="source" /> is invalid and could not be decompressed, or the window size it declares is greater than the configured maximum.</remarks>
public OperationStatus Decompress(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesConsumed, out int bytesWritten)
{
EnsureInitialized();
Debug.Assert(_state != null);

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
internal enum BrotliDecoderParameter
{
DisableRingBufferReallocation,
LargeWindow
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides decompression options to be used with <see cref="BrotliStream"/>.
/// </summary>
public sealed class BrotliDecompressionOptions
{
private int _maxWindowLog2 = BrotliUtils.WindowBits_DefaultLarge;

/// <summary>
/// Gets or sets the maximum base-2 logarithm of the window size accepted when decompressing.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException" accessor="set">The value is less than 10 or greater than 30.</exception>
/// <remarks>
/// 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.
/// </remarks>
public int MaxWindowLog2
{
get => _maxWindowLog2;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, BrotliUtils.WindowBits_Min, nameof(value));
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, BrotliUtils.WindowBits_MaxLarge, nameof(value));

_maxWindowLog2 = value;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ public sealed partial class BrotliStream : Stream
private bool _nonEmptyInput;
private volatile bool _decompressionFinished;

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

_decoder.SetMaxWindowLog2(decompressionOptions.MaxWindowLog2);
}

/// <summary>Reads a number of decompressed bytes into the specified byte array.</summary>
/// <param name="buffer">The array used to store decompressed bytes.</param>
/// <param name="offset">The byte offset in <paramref name="buffer" /> at which the read bytes will be placed.</param>
Expand Down
Loading
Loading