Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ public sealed class ZstandardCompressedContent : HttpContent
{
private const string Encoding = "zstd";

// RFC 9659 requires zstd decoders for the "zstd" content coding to support a window size of at
// least 8 MB (2^23) and recommends that encoders not produce frames requiring a larger window.
// Some compression levels (notably CompressionLevel.SmallestSize) would otherwise select a larger
// window, producing payloads that a conformant server would reject. See RFC 9659, Section 3.
private const int RfcMaxWindowLog2 = 23;
private static readonly ZstandardCompressionOptions s_smallestSizeRfcOptions = new ZstandardCompressionOptions
{
Quality = ZstandardCompressionOptions.MaxQuality,
WindowLog2 = RfcMaxWindowLog2
};
Comment thread
rzikm marked this conversation as resolved.

private readonly HttpContent _content;
private readonly ZstandardCompressionOptions? _compressionOptions;
private readonly CompressionLevel _compressionLevel;
Expand All @@ -28,12 +39,27 @@ public sealed class ZstandardCompressedContent : HttpContent
/// </summary>
/// <param name="content">The HTTP content to compress.</param>
/// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression efficiency.</param>
/// <remarks>
/// RFC 9659 requires that the "zstd" content coding be decodable with a window size of 8 MB (2^23) and
/// recommends that encoders not produce frames requiring a larger window. When setting <paramref name="compressionLevel"/>
/// to <see cref="CompressionLevel.SmallestSize"/>, this class applies RFC-compliant options to limit the window size
/// so that the produced content is accepted by servers that enforce this limit.
/// </remarks>
public ZstandardCompressedContent(HttpContent content, CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
ArgumentNullException.ThrowIfNull(content);
CompressedContentCore.ValidateCompressionLevel(compressionLevel, nameof(compressionLevel));

_compressionLevel = compressionLevel;
if (compressionLevel == CompressionLevel.SmallestSize)
{
// use RFC-compliant options for SmallestSize to avoid producing frames that a conformant server would reject
_compressionOptions = s_smallestSizeRfcOptions;
}
else
{
_compressionLevel = compressionLevel;
}

_content = content;
CompressedContentCore.InitializeHeaders(this, content, Encoding);
}
Expand All @@ -44,6 +70,12 @@ public ZstandardCompressedContent(HttpContent content, CompressionLevel compress
/// </summary>
/// <param name="content">The HTTP content to compress.</param>
/// <param name="compressionOptions">The options used to fine-tune the compression.</param>
/// <remarks>
/// RFC 9659 requires that the "zstd" content coding be decodable with a window size of 8 MB (2^23) and
/// recommends that encoders not produce frames requiring a larger window. When supplying custom options,
/// consider limiting <see cref="ZstandardCompressionOptions.WindowLog2"/> to 23 or less so that the
/// produced content is accepted by servers that enforce this limit.
/// </remarks>
public ZstandardCompressedContent(HttpContent content, ZstandardCompressionOptions compressionOptions)
{
ArgumentNullException.ThrowIfNull(content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xunit;
using Microsoft.DotNet.XUnitExtensions;

namespace System.Net.Http.Functional.Tests
{
Expand Down Expand Up @@ -104,15 +106,29 @@ public async Task BrotliZstd_SerializeToStream_WithOptions_RoundTrips(string enc
Assert.Equal(original, DecompressBrotliOrZstd(await SerializeAsync(content, async: true), encoding));
}

[Theory]
[InlineData("br")]
[InlineData("zstd")]
public async Task BrotliZstd_SerializeToStream_WithCompressionLevel_RoundTrips(string encoding)
[ConditionalTheory]
[InlineData("br", CompressionLevel.NoCompression)]
[InlineData("br", CompressionLevel.Fastest)]
[InlineData("br", CompressionLevel.Optimal)]
[InlineData("br", CompressionLevel.SmallestSize)]
[InlineData("zstd", CompressionLevel.NoCompression)]
[InlineData("zstd", CompressionLevel.Fastest)]
[InlineData("zstd", CompressionLevel.Optimal)]
[InlineData("zstd", CompressionLevel.SmallestSize)]
public async Task BrotliZstd_SerializeToStream_WithCompressionLevel_RoundTrips(string encoding, CompressionLevel compressionLevel)
{
byte[] original = Encoding.UTF8.GetBytes(new string('a', 4096));
if (PlatformDetection.Is32BitProcess && compressionLevel == CompressionLevel.SmallestSize && encoding == "zstd")
{
// Zstandard smallest size requires too much working memory
// (800+ MB) and causes intermittent allocation errors on 32-bit
// processes in CI.
throw new SkipTestException($"Skipping {encoding} with {compressionLevel} on 32-bit process due to excessive memory requirements.");
}

byte[] original = Encoding.UTF8.GetBytes(string.Concat(Enumerable.Repeat("The quick brown fox jumps over the lazy dog. ", 4096)));
HttpContent content = encoding == "br"
? new BrotliCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize)
: new ZstandardCompressedContent(new ByteArrayContent(original), CompressionLevel.SmallestSize);
? new BrotliCompressedContent(new ByteArrayContent(original), compressionLevel)
: new ZstandardCompressedContent(new ByteArrayContent(original), compressionLevel);

Assert.Equal(original, DecompressBrotliOrZstd(await SerializeAsync(content, async: true), encoding));
}
Expand Down Expand Up @@ -145,7 +161,8 @@ private static byte[] DecompressBrotliOrZstd(byte[] compressed, string encoding)
using Stream decompressor = encoding switch
{
"br" => new BrotliStream(source, CompressionMode.Decompress),
"zstd" => new ZstandardStream(source, CompressionMode.Decompress),
// RFC 9659 requires the "zstd" content coding to be decodable with an 8 MB (2^23) window.
"zstd" => new ZstandardStream(source, new ZstandardDecompressionOptions { MaxWindowLog2 = 23 }),
_ => throw new ArgumentOutOfRangeException(nameof(encoding))
};

Expand Down