You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ZSTD_decompressStream returns 0 at the end of each frame, not the end of the
stream, but ZstandardDecoder.Decompress treated that as end-of-stream via a
permanent _finished latch. A zstd stream made of multiple concatenated frames
(valid per RFC 8878) -- e.g. large HTTP Content-Encoding: zstd responses --
was therefore silently truncated to its first frame, surfacing downstream as
truncated payloads (e.g. JSON parse errors at exactly 65536 bytes).
ZstandardStream now continues decoding subsequent frames on the same native
context (mirroring GZipStream multi-member handling), distinguishes a following
frame from trailing data via the frame magic number, and only treats
end-of-input as a clean end when at a frame boundary. Adds regression tests
for concatenated frames (buffered, split across reads, and with trailing data).
Fixes#129038.
Copy file name to clipboardExpand all lines: src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardDecoder.cs
+16Lines changed: 16 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -323,6 +323,22 @@ public void Reset()
323
323
_finished=false;
324
324
}
325
325
326
+
/// <summary>
327
+
/// Clears the end-of-frame state so the decoder can continue decoding the next frame
328
+
/// in a stream that contains multiple concatenated Zstandard frames (valid per RFC 8878 §3).
329
+
/// </summary>
330
+
/// <remarks>
331
+
/// <see cref="Interop.Zstd.ZSTD_decompressStream"/> returns 0 at the end of each frame, not at the
332
+
/// end of the stream, and the native context is automatically ready to begin the next frame on the
333
+
/// following call. Only the managed end-of-frame latch is cleared here; the native context is left
334
+
/// intact so window-size and dictionary settings carry over to the next frame. Must only be called
335
+
/// after <see cref="Decompress"/> reported <see cref="OperationStatus.Done"/>.
336
+
/// </remarks>
337
+
internalvoidPrepareForNextFrame()
338
+
{
339
+
_finished=false;
340
+
}
341
+
326
342
/// <summary>References a prefix for the next decompression operation.</summary>
327
343
/// <remarks>The prefix will be used only for the next decompression frame and will be removed when <see cref="Reset"/> is called. The referenced data must remain valid and unmodified for the duration of the decompression operation.</remarks>
328
344
/// <exception cref="ObjectDisposedException">The decoder has been disposed.</exception>
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
4
usingSystem.Buffers;
5
+
usingSystem.Buffers.Binary;
5
6
usingSystem.Diagnostics;
6
7
usingSystem.Diagnostics.CodeAnalysis;
7
8
usingSystem.Threading;
@@ -14,6 +15,15 @@ public sealed partial class ZstandardStream
14
15
privateZstandardDecoder?_decoder;
15
16
privatebool_nonEmptyInput;
16
17
18
+
// Set when the decoder reports the end of a frame (OperationStatus.Done). A zstd stream may
19
+
// contain multiple frames concatenated back-to-back (RFC 8878 §3), so reaching the end of a
20
+
// frame is not necessarily the end of the stream. While at a frame boundary, a subsequent
21
+
// end-of-input from the underlying stream is a clean end rather than truncated data.
22
+
privatebool_atFrameBoundary;
23
+
24
+
// Length of a Zstandard frame magic number, in bytes.
25
+
privateconstintZstdFrameMagicLength=sizeof(uint);
26
+
17
27
/// <summary>Initializes a new instance of the <see cref="ZstandardStream" /> class by using the specified stream and decoder instance.</summary>
18
28
/// <param name="stream">The stream from which data to decompress is read.</param>
19
29
/// <param name="decoder">The decoder instance to use for decompression. The stream will not dispose this decoder; instead, it will reset it when the stream is disposed.</param>
@@ -36,44 +46,124 @@ private bool TryDecompress(Span<byte> destination, out int bytesWritten, out Ope
0 commit comments