Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
10 changes: 6 additions & 4 deletions src/SharpCompress/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ public static class Constants
/// by rewinding and re-reading the same data.
/// </para>
/// <para>
/// <b>Default:</b> 163840 bytes (160KB) - sized to cover ZStandard's worst-case
/// first block on a tar archive (~131KB including frame header overhead).
/// ZStandard blocks can be up to 128KB, exceeding the previous 81KB default.
/// <b>Default:</b> 81920 bytes (80KB) — sufficient for most formats.
/// Formats that require larger buffers (e.g. BZip2, ZStandard) declare their
/// own minimum via <c>TarWrapper.MinimumRewindBufferSize</c>, and
/// <c>TarWrapper.MaximumRewindBufferSize</c> is used at stream construction
/// to ensure the correct capacity is allocated upfront.
Comment on lines +28 to +29

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These updated docs state that TarWrapper.MaximumRewindBufferSize is used “at stream construction” to ensure correct capacity is allocated upfront, but TarReader now grows the buffer lazily via EnsureMinimumRingBufferCapacity after wrapper matching (and TarFactory allocates during StartRecording, not construction). Consider adjusting the wording to reflect the actual behavior so consumers aren’t misled about when/where the max size is applied.

Suggested change
/// <c>TarWrapper.MaximumRewindBufferSize</c> is used at stream construction
/// to ensure the correct capacity is allocated upfront.
/// <c>TarWrapper.MaximumRewindBufferSize</c> may be used to grow the rewind
/// buffer as needed during format detection or recording rather than
/// allocating the maximum capacity upfront at stream construction.

Copilot uses AI. Check for mistakes.
/// </para>
/// <para>
/// <b>Typical usage:</b> 500-1000 bytes for most archives
Expand All @@ -41,7 +43,7 @@ public static class Constants
/// </list>
/// </para>
/// </remarks>
public static int RewindableBufferSize { get; set; } = 163840;
public static int RewindableBufferSize { get; set; } = 81920;

public static CultureInfo DefaultCultureInfo { get; set; } = CultureInfo.InvariantCulture;
}
12 changes: 11 additions & 1 deletion src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,17 @@ internal IEnumerable<ZipHeader> ReadStreamHeader(Stream stream)
}
else
{
headerBytes = reader.ReadUInt32();
try
{
headerBytes = reader.ReadUInt32();
}
catch (EndOfStreamException ex)
{
throw new InvalidFormatException(
"Unexpected end of stream while reading ZIP archive",
ex
);
}
}

_lastEntryHeader = null;
Expand Down
16 changes: 16 additions & 0 deletions src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,10 @@ cache misses.
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = await BsRAsync(zn, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -465,6 +469,10 @@ cache misses.
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = await BsRAsync(zn, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -542,6 +550,10 @@ cache misses.
BlockOverrun();
}

if (nextSym - 1 < 0 || nextSym - 1 >= yy.Length)
{
throw new InvalidFormatException("BZip2: symbol out of range");
}
tmp = yy[nextSym - 1];
unzftab[seqToUnseq[tmp]]++;
ll8[last] = seqToUnseq[tmp];
Expand Down Expand Up @@ -578,6 +590,10 @@ hence the unrolling.
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = await BsRAsync(zn, cancellationToken).ConfigureAwait(false);
Expand Down
16 changes: 16 additions & 0 deletions src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,10 @@ cache misses.
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = BsR(zn);
Expand Down Expand Up @@ -788,6 +792,10 @@ cache misses.
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = BsR(zn);
Expand Down Expand Up @@ -862,6 +870,10 @@ cache misses.
BlockOverrun();
}

if (nextSym - 1 < 0 || nextSym - 1 >= yy.Length)
{
throw new InvalidFormatException("BZip2: symbol out of range");
}
tmp = yy[nextSym - 1];
unzftab[seqToUnseq[tmp]]++;
ll8[last] = seqToUnseq[tmp];
Expand Down Expand Up @@ -898,6 +910,10 @@ hence the unrolling.
groupPos = BZip2Constants.G_SIZE;
}
groupPos--;
if (groupNo < 0 || groupNo >= selector.Length)
{
throw new InvalidFormatException("BZip2: group selector out of range");
}
zt = selector[groupNo];
zn = minLens[zt];
zvec = BsR(zn);
Expand Down
4 changes: 4 additions & 0 deletions src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ private void CreateTable()
overflowBits--;
} while (overflowBits != 0);

if (index < 0 || index >= array.Length)
{
throw new InvalidFormatException("Deflate64: invalid Huffman data");
}
array[index] = (short)ch;
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ private async Task<int> NEXTBYTEAsync(CancellationToken cancellationToken)
{
if (inByteCount == compressedSize)
{
_inputExhausted = true;
return EOF;
}

Expand All @@ -35,6 +36,7 @@ private async Task<int> NEXTBYTEAsync(CancellationToken cancellationToken)
.ConfigureAwait(false);
if (bytesRead == 0)
{
_inputExhausted = true;
return EOF;
}

Expand Down Expand Up @@ -117,6 +119,13 @@ CancellationToken cancellationToken
{
if (length == 0)
{
if (_inputExhausted && bitBufferCount <= 0)
{
throw new InvalidFormatException(
"ReduceStream: compressed data exhausted before uncompressed size reached"
);
}

byte nextByte = await GetNextByteAsync(cancellationToken).ConfigureAwait(false);
if (nextByte != RunLengthCode)
{
Expand Down
17 changes: 16 additions & 1 deletion src/SharpCompress/Compressors/Reduce/ReduceStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,24 @@ public override long Position

private int bitBufferCount;
private ulong bitBuffer;
private bool _inputExhausted;

private int NEXTBYTE()
{
if (inByteCount == compressedSize)
{
_inputExhausted = true;
return EOF;
}

inByteCount++;
return inStream.ReadByte();
int b = inStream.ReadByte();
if (b < 0)
{
_inputExhausted = true;
return EOF;
}
return b;
}

private void READBITS(int nbits, out byte zdest)
Expand Down Expand Up @@ -208,6 +216,13 @@ public override int Read(byte[] buffer, int offset, int count)
{
if (length == 0)
{
if (_inputExhausted && bitBufferCount <= 0)
{
throw new InvalidFormatException(
"ReduceStream: compressed data exhausted before uncompressed size reached"
);
}

byte nextByte = GetNextByte();
if (nextByte != RunLengthCode)
{
Expand Down
52 changes: 9 additions & 43 deletions src/SharpCompress/Compressors/Shrink/ShrinkStream.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,18 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;

namespace SharpCompress.Compressors.Shrink;

internal partial class ShrinkStream : Stream
{
internal static async ValueTask<ShrinkStream> CreateAsync(
Stream stream,
CompressionMode compressionMode,
long compressedSize,
long uncompressedSize,
CancellationToken cancellationToken = default
)
{
var shrinkStream = new ShrinkStream(
stream,
compressionMode,
compressedSize,
uncompressedSize
);
var shrinkStream = new ShrinkStream(stream, uncompressedSize);
await shrinkStream.DecompressAsync(cancellationToken).ConfigureAwait(false);
return shrinkStream;
}
Expand All @@ -33,42 +25,16 @@ private async Task DecompressAsync(CancellationToken cancellationToken)
return;
}

// Read all compressed data asynchronously
var src = new byte[_compressedSize];
int bytesRead = 0;
int totalBytesRead = 0;

while (totalBytesRead < (int)_compressedSize)
{
bytesRead = await inStream
.ReadAsync(
src,
totalBytesRead,
(int)_compressedSize - totalBytesRead,
cancellationToken
)
.ConfigureAwait(false);
if (bytesRead == 0)
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading compressed data"
);
}
totalBytesRead += bytesRead;
}
// Read actual compressed data from the stream rather than pre-allocating based on the
// declared compressed size, which may be crafted to cause an OutOfMemoryException.
// The stream is already bounded by ReadOnlySubStream in ZipFilePart.
using var srcMs = new MemoryStream();
await _inStream.CopyToAsync(srcMs, 81920, cancellationToken).ConfigureAwait(false);
var src = srcMs.ToArray();
var srcLen = src.Length;

Comment thread
adamhathcock marked this conversation as resolved.
// Decompress synchronously (CPU-bound operation)
var srcUsed = 0;
var dstUsed = 0;

HwUnshrink.Unshrink(
src,
(int)_compressedSize,
out srcUsed,
_byteOut,
(int)_uncompressedSize,
out dstUsed
);
HwUnshrink.Unshrink(src, srcLen, out _, _byteOut, (int)_uncompressedSize, out var dstUsed);
_outBytesCount = dstUsed;
_decompressed = true;
}
Expand Down
50 changes: 24 additions & 26 deletions src/SharpCompress/Compressors/Shrink/ShrinkStream.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
using System;
using System.IO;
using SharpCompress;
using SharpCompress.Common;

namespace SharpCompress.Compressors.Shrink;

internal partial class ShrinkStream : Stream
{
private Stream inStream;
private readonly Stream _inStream;

private ulong _compressedSize;
private long _uncompressedSize;
private byte[] _byteOut;
private readonly long _uncompressedSize;
private readonly byte[] _byteOut;
private long _outBytesCount;
private bool _decompressed;
private long _position;

public ShrinkStream(
Stream stream,
CompressionMode compressionMode,
long compressedSize,
long uncompressedSize
)
public ShrinkStream(Stream stream, long uncompressedSize)
{
inStream = stream;
if (uncompressedSize > int.MaxValue)
{
throw new InvalidFormatException(
$"Shrink: declared uncompressed size {uncompressedSize} exceeds maximum supported size."
);
}

_inStream = stream;

_compressedSize = (ulong)compressedSize;
_uncompressedSize = uncompressedSize;
_byteOut = new byte[_uncompressedSize];
_byteOut = new byte[(int)_uncompressedSize];
_outBytesCount = 0L;
Comment thread
adamhathcock marked this conversation as resolved.
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}

public override bool CanRead => true;

public override bool CanSeek => true;
Expand All @@ -55,18 +50,21 @@ public override int Read(byte[] buffer, int offset, int count)
{
if (!_decompressed)
{
var src = new byte[_compressedSize];
inStream.ReadExact(src, 0, (int)_compressedSize);
var srcUsed = 0;
var dstUsed = 0;
// Read actual compressed data from the stream rather than pre-allocating based on the
// declared compressed size, which may be crafted to cause an OutOfMemoryException.
// The stream is already bounded by ReadOnlySubStream in ZipFilePart.
using var srcMs = new MemoryStream();
_inStream.CopyTo(srcMs);
var src = srcMs.ToArray();
var srcLen = src.Length;

Comment thread
adamhathcock marked this conversation as resolved.
HwUnshrink.Unshrink(
src,
(int)_compressedSize,
out srcUsed,
srcLen,
out _,
_byteOut,
(int)_uncompressedSize,
out dstUsed
out var dstUsed
);
_outBytesCount = dstUsed;
_decompressed = true;
Expand Down
13 changes: 13 additions & 0 deletions src/SharpCompress/Compressors/ZStandard/ZstandardConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,17 @@ internal class ZstandardConstants
/// Magic number found at start of ZStandard frame: 0xFD 0x2F 0xB5 0x28
/// </summary>
public const uint MAGIC = 0xFD2FB528;

/// <summary>
/// Maximum uncompressed size of a single ZStandard block: ZSTD_BLOCKSIZE_MAX = 128 KB.
/// </summary>
public const int BlockSizeMax = 1 << 17; // 131072 bytes

/// <summary>
/// Recommended input (compressed) buffer size for streaming decompression:
/// ZSTD_DStreamInSize = ZSTD_BLOCKSIZE_MAX + ZSTD_blockHeaderSize (3 bytes).
/// The ring buffer must be at least this large to hold the compressed bytes read
/// during format detection before the first rewind.
/// </summary>
public const int DStreamInSize = BlockSizeMax + 3;
}
Loading
Loading