Merge remote-tracking branch 'origin/release'

# Conflicts:
#	src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs
#	src/SharpCompress/Compressors/LZMA/LzmaStream.cs
This commit is contained in:
Adam Hathcock
2026-10-19 11:15:09 +01:00
25 changed files with 1249 additions and 119 deletions

View File

@@ -37,6 +37,12 @@ public partial class ArcFilePart
.ConfigureAwait(false);
break;
case CompressionType.Crunched:
if (Header.CompressedSize > int.MaxValue)
{
throw new InvalidFormatException(
"ARC: compressed size exceeds the supported range."
);
}
if (Header.OriginalSize > 128 * 1024)
{
throw new NotSupportedException(

View File

@@ -52,6 +52,12 @@ public partial class ArcFilePart : FilePart
compressedStream = SqueezeStream.Create(_stream, (int)Header.CompressedSize);
break;
case CompressionType.Crunched:
if (Header.CompressedSize > int.MaxValue)
{
throw new InvalidFormatException(
"ARC: compressed size exceeds the supported range."
);
}
if (Header.OriginalSize > 128 * 1024)
{
throw new NotSupportedException(

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.IO;
@@ -8,7 +9,9 @@ internal partial class Zip64DirectoryEndHeader
{
internal override async ValueTask Read(AsyncBinaryReader reader)
{
SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async().ConfigureAwait(false);
SizeOfDirectoryEndRecord = ValidateRecordSize(
await reader.ReadUInt64Async().ConfigureAwait(false)
);
VersionMadeBy = await reader.ReadUInt16Async().ConfigureAwait(false);
VersionNeededToExtract = await reader.ReadUInt16Async().ConfigureAwait(false);
VolumeNumber = await reader.ReadUInt32Async().ConfigureAwait(false);
@@ -18,10 +21,25 @@ internal partial class Zip64DirectoryEndHeader
DirectorySize = (long)await reader.ReadUInt64Async().ConfigureAwait(false);
DirectoryStartOffsetRelativeToDisk = (long)
await reader.ReadUInt64Async().ConfigureAwait(false);
var size = (int)(
SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
);
DataSector = new byte[size];
await reader.ReadBytesAsync(DataSector, 0, size).ConfigureAwait(false);
var remaining =
SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS;
if (remaining == 0)
{
return;
}
var buffer = _pool.Rent(4096);
try
{
while (remaining > 0)
{
var size = (int)Math.Min(4096, remaining);
await reader.ReadBytesAsync(buffer, 0, size).ConfigureAwait(false);
remaining -= size;
}
}
finally
{
_pool.Return(buffer);
}
}
}

View File

@@ -1,15 +1,22 @@
using System;
using System.Buffers;
using System.IO;
namespace SharpCompress.Common.Zip.Headers;
internal partial class Zip64DirectoryEndHeader : ZipHeader
{
private readonly ArrayPool<byte> _pool;
public Zip64DirectoryEndHeader()
: base(ZipHeaderType.Zip64DirectoryEnd) { }
: this(ArrayPool<byte>.Shared) { }
internal Zip64DirectoryEndHeader(ArrayPool<byte> pool)
: base(ZipHeaderType.Zip64DirectoryEnd) => _pool = pool;
internal override void Read(BinaryReader reader)
{
SizeOfDirectoryEndRecord = (long)reader.ReadUInt64();
SizeOfDirectoryEndRecord = ValidateRecordSize(reader.ReadUInt64());
VersionMadeBy = reader.ReadUInt16();
VersionNeededToExtract = reader.ReadUInt16();
VolumeNumber = reader.ReadUInt32();
@@ -18,12 +25,42 @@ internal partial class Zip64DirectoryEndHeader : ZipHeader
TotalNumberOfEntries = (long)reader.ReadUInt64();
DirectorySize = (long)reader.ReadUInt64();
DirectoryStartOffsetRelativeToDisk = (long)reader.ReadUInt64();
DataSector = reader.ReadBytes(
(int)(
SizeOfDirectoryEndRecord
- SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
)
);
// Extensible data is not interpreted. Consume it without allocating from its declared size.
var remaining =
SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS;
if (remaining == 0)
{
return;
}
var buffer = _pool.Rent(4096);
try
{
while (remaining > 0)
{
var read = reader.Read(buffer, 0, (int)Math.Min(4096, remaining));
if (read == 0)
{
throw new IncompleteArchiveException("Truncated ZIP64 extensible data.");
}
remaining -= read;
}
}
finally
{
_pool.Return(buffer);
}
}
private static long ValidateRecordSize(ulong size)
{
if (
size < SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
|| size > long.MaxValue
)
{
throw new InvalidFormatException("Invalid ZIP64 end record size.");
}
return (long)size;
}
private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44;
@@ -45,6 +82,4 @@ internal partial class Zip64DirectoryEndHeader : ZipHeader
public long DirectorySize { get; private set; }
public long DirectoryStartOffsetRelativeToDisk { get; private set; }
public byte[]? DataSector { get; private set; }
}

View File

@@ -1,8 +1,10 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Compressors.RLE90;
using SharpCompress.IO;
namespace SharpCompress.Compressors.ArcLzw;
@@ -15,37 +17,19 @@ public partial class ArcLzwStream
CancellationToken cancellationToken
)
{
if (_processed)
cancellationToken.ThrowIfCancellationRequested();
if (count == 0)
{
return 0;
}
_processed = true;
var data = new byte[_compressedSize];
int totalRead = 0;
while (totalRead < _compressedSize)
if (!_processed)
{
int read = await _stream
.ReadAsync(data, totalRead, _compressedSize - totalRead, cancellationToken)
.ConfigureAwait(false);
if (read == 0)
{
break;
}
totalRead += read;
using var limited = new ReadOnlySubStream(_stream, _compressedSize);
using var data = new PooledMemoryStream();
await limited.CopyToAsync(data, 81920, cancellationToken).ConfigureAwait(false);
FinishDecompression(data);
}
var decoded = Decompress(data, _useCrunched);
var result = decoded.Count;
if (_useCrunched)
{
var unpacked = RLE.UnpackRLE(decoded.ToArray());
unpacked.CopyTo(buffer, 0);
result = unpacked.Count;
}
else
{
decoded.CopyTo(buffer, 0);
}
return result;
return Read(buffer, offset, count);
}
#if !LEGACY_DOTNET

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using SharpCompress.Common;
using SharpCompress.Compressors.RLE90;
using SharpCompress.IO;
namespace SharpCompress.Compressors.ArcLzw;
@@ -13,6 +14,8 @@ public partial class ArcLzwStream : Stream
private bool _processed;
private bool _useCrunched;
private int _compressedSize;
private byte[]? _decoded;
private int _decodedPosition;
private const int BITS = 12;
private const int CRUNCH_BITS = 12;
@@ -34,6 +37,10 @@ public partial class ArcLzwStream : Stream
public ArcLzwStream(Stream stream, int compressedSize, bool useCrunched = true)
{
if (compressedSize < 0)
{
throw new InvalidFormatException("ARC: invalid compressed size.");
}
_stream = stream;
_useCrunched = useCrunched;
_compressedSize = compressedSize;
@@ -177,28 +184,35 @@ public partial class ArcLzwStream : Stream
public override int Read(byte[] buffer, int offset, int count)
{
if (_processed)
if (count == 0)
{
return 0;
}
_processed = true;
var data = new byte[_compressedSize];
_stream.Read(data, 0, _compressedSize);
var decoded = Decompress(data, _useCrunched);
var result = decoded.Count;
if (_useCrunched)
if (!_processed)
{
var unpacked = RLE.UnpackRLE(decoded.ToArray());
unpacked.CopyTo(buffer, 0);
result = unpacked.Count;
}
else
{
decoded.CopyTo(buffer, 0);
using var limited = new ReadOnlySubStream(_stream, _compressedSize);
using var data = new PooledMemoryStream();
limited.CopyTo(data);
FinishDecompression(data);
}
var result = Math.Min(count, _decoded!.Length - _decodedPosition);
Buffer.BlockCopy(_decoded, _decodedPosition, buffer, offset, result);
_decodedPosition += result;
return result;
}
private void FinishDecompression(PooledMemoryStream data)
{
if (data.Length != _compressedSize)
{
throw new IncompleteArchiveException("Truncated ARC compressed data.");
}
// Buffer only bytes actually read, never the size claimed by the ARC header.
var decoded = Decompress(data.ToArray(), _useCrunched);
_decoded = _useCrunched ? RLE.UnpackRLE(decoded.ToArray()).ToArray() : decoded.ToArray();
_processed = true;
}
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotImplementedException();

View File

@@ -95,6 +95,7 @@ internal partial class OutWindow : IAsyncDisposable
var fastSize = Math.Min(Math.Min(sizeUntilWindowEnd, sizeUntilOverlap), targetSize);
if (fastSize >= 2)
{
EnsureCapacity(_pos + fastSize);
_buffer.AsSpan(pos, fastSize).CopyTo(_buffer.AsSpan(_pos, fastSize));
_pos += fastSize;
pos += fastSize;
@@ -120,6 +121,7 @@ internal partial class OutWindow : IAsyncDisposable
public async ValueTask PutByteAsync(byte b, CancellationToken cancellationToken = default)
{
EnsureCapacity(_pos + 1);
_buffer[_pos++] = b;
_total++;
if (_pos >= _windowSize)
@@ -139,7 +141,7 @@ internal partial class OutWindow : IAsyncDisposable
{
cancellationToken.ThrowIfCancellationRequested();
var curSize = _windowSize - _pos;
var curSize = Math.Min(4096, _windowSize - _pos);
if (curSize > _limit - _total)
{
curSize = (int)(_limit - _total);
@@ -148,6 +150,7 @@ internal partial class OutWindow : IAsyncDisposable
{
curSize = size;
}
EnsureCapacity(_pos + curSize);
var numReadBytes = await stream
.ReadAsync(_buffer, _pos, curSize, cancellationToken)
.ConfigureAwait(false);
@@ -173,7 +176,7 @@ internal partial class OutWindow : IAsyncDisposable
stream.Position = len - size;
_total = 0;
_limit = size;
_pos = _windowSize - size;
_pos = 0;
await CopyStreamAsync(stream, size).ConfigureAwait(false);
if (_pos == _windowSize)
{

View File

@@ -51,6 +51,8 @@ internal partial class OutWindow : IDisposable
internal int FastWindowSize => _windowSize;
internal long FastLimit => _limit;
internal void EnsureFastCapacity() => EnsureCapacity(_pos + 1);
internal void FastFlush() => Flush();
internal void SetPendingFast(int distance, int len)
@@ -60,23 +62,24 @@ internal partial class OutWindow : IDisposable
}
#endif
internal int BufferCapacity => _buffer?.Length ?? 0;
public void Create(int windowSize)
{
if (windowSize <= 0)
{
throw new InvalidFormatException($"LZMA: invalid dictionary size {windowSize}");
}
if (_windowSize != windowSize)
if (_windowSize != windowSize || _buffer is null)
{
// Allocate first so failed growth leaves the existing buffer owned by this window.
var replacement = _pool.Rent(windowSize);
var replacement = _pool.Rent(Math.Min(windowSize, 4096));
if (_buffer is not null)
{
_pool.Return(_buffer);
}
_buffer = replacement;
}
_buffer[windowSize - 1] = 0;
_windowSize = windowSize;
_pos = 0;
_streamPos = 0;
@@ -85,6 +88,20 @@ internal partial class OutWindow : IDisposable
_limit = 0;
}
private void EnsureCapacity(int required)
{
if (required <= _buffer.Length)
{
return;
}
// Grow from emitted history, never from the advertised dictionary or output size.
var capacity = (int)Math.Min(_windowSize, Math.Max(required, (long)_buffer.Length * 2));
var replacement = _pool.Rent(capacity);
_buffer.AsSpan(0, _pos).CopyTo(replacement);
_pool.Return(_buffer);
_buffer = replacement;
}
public void Dispose()
{
try
@@ -122,7 +139,7 @@ internal partial class OutWindow : IDisposable
stream.Position = len - size;
_total = 0;
_limit = size;
_pos = _windowSize - size;
_pos = 0;
CopyStream(stream, size);
if (_pos == _windowSize)
{
@@ -186,6 +203,7 @@ internal partial class OutWindow : IDisposable
var fastSize = Math.Min(Math.Min(sizeUntilWindowEnd, sizeUntilOverlap), targetSize);
if (fastSize >= 2)
{
EnsureCapacity(_pos + fastSize);
_buffer.AsSpan(pos, fastSize).CopyTo(_buffer.AsSpan(_pos, fastSize));
_pos += fastSize;
pos += fastSize;
@@ -211,6 +229,7 @@ internal partial class OutWindow : IDisposable
public void PutByte(byte b)
{
EnsureCapacity(_pos + 1);
_buffer[_pos++] = b;
_total++;
if (_pos >= _windowSize)
@@ -221,6 +240,11 @@ internal partial class OutWindow : IDisposable
public byte GetByte(int distance)
{
// The initial literal context is zero; no dictionary history has been emitted yet.
if (_total == 0 && distance == 0)
{
return 0;
}
var pos = _pos - distance - 1;
if (pos < 0)
{
@@ -234,7 +258,7 @@ internal partial class OutWindow : IDisposable
var size = len;
while (size > 0 && _pos < _windowSize && _total < _limit)
{
var curSize = _windowSize - _pos;
var curSize = Math.Min(4096, _windowSize - _pos);
if (curSize > _limit - _total)
{
curSize = (int)(_limit - _total);
@@ -243,6 +267,7 @@ internal partial class OutWindow : IDisposable
{
curSize = size;
}
EnsureCapacity(_pos + curSize);
var numReadBytes = stream.Read(_buffer, _pos, curSize);
if (numReadBytes == 0)
{

View File

@@ -151,6 +151,10 @@ public partial class Decoder
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte GetByte(int distance)
{
if (Total == 0 && distance == 0)
{
return 0;
}
// A previous decoding failure can leave a rep distance outside the dictionary.
// Unlike a managed array access, this pointer read has no implicit bounds check.
if ((uint)distance >= (uint)WindowSize)
@@ -228,11 +232,31 @@ public partial class Decoder
OutWindow outWindow,
RangeCoder.Decoder rangeDecoder
)
{
do
{
outWindow.CopyPending();
if (!outWindow.HasSpace)
{
return false;
}
// Grow only after filling the current allocation, and never while it is pinned.
outWindow.EnsureFastCapacity();
if (CodeFastCore(dictionarySize, outWindow, rangeDecoder))
{
return true;
}
} while (outWindow.HasSpace);
return false;
}
private unsafe bool CodeFastCore(
int dictionarySize,
OutWindow outWindow,
RangeCoder.Decoder rangeDecoder
)
{
var dictionarySizeCheck = Math.Max(dictionarySize, 1);
outWindow.CopyPending();
var dic = outWindow.FastBuffer;
var inBuf = rangeDecoder.FastBufferArray;
bool result;
@@ -270,7 +294,11 @@ public partial class Decoder
Pos = outWindow.FastPos,
Total = outWindow.FastTotal,
WindowSize = outWindow.FastWindowSize,
Limit = outWindow.FastLimit,
// The next decode call can grow the dictionary after this pin is released.
Limit = Math.Min(
outWindow.FastLimit,
outWindow.FastTotal + dic.Length - outWindow.FastPos
),
OutWindow = outWindow,
};

View File

@@ -215,6 +215,11 @@ public partial class LzmaStream
}
}
if (_availableBytes < 0)
{
throw new DataErrorException();
}
var toProcess = count - total;
if (toProcess > _availableBytes)
{
@@ -238,6 +243,10 @@ public partial class LzmaStream
}
var read = _outWindow.Read(buffer, offset, toProcess);
if (read == 0 && _availableBytes > 0)
{
throw new DataErrorException();
}
total += read;
offset += read;
_position += read;
@@ -337,6 +346,11 @@ public partial class LzmaStream
}
}
if (_availableBytes < 0)
{
throw new DataErrorException();
}
var toProcess = count - total;
if (toProcess > _availableBytes)
{
@@ -360,6 +374,10 @@ public partial class LzmaStream
}
var read = _outWindow.Read(buffer, offset, toProcess);
if (read == 0 && _availableBytes > 0)
{
throw new DataErrorException();
}
total += read;
offset += read;
_position += read;

View File

@@ -73,6 +73,11 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
}
else
{
// Larger standard dictionaries exceed this decoder's positive Int32 window range.
if (properties[0] > 37)
{
throw new InvalidParamException();
}
_dictionarySize = 2 | (properties[0] & 1);
_dictionarySize <<= (properties[0] >> 1) + 11;
@@ -338,6 +343,11 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
}
}
if (_availableBytes < 0)
{
throw new DataErrorException();
}
var toProcess = count - total;
if (toProcess > _availableBytes)
{
@@ -355,6 +365,11 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
}
var read = _outWindow.Read(buffer, offset, toProcess);
// Unknown-size raw LZMA can end without output; otherwise no progress is corruption.
if (read == 0 && _availableBytes > 0)
{
throw new DataErrorException();
}
total += read;
offset += read;
_position += read;
@@ -507,10 +522,21 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
}
}
private int ReadChunkHeaderByte()
{
var value = _inputStream!.ReadByte();
// EOF is not an LZMA2 end marker and must never enter chunk-size arithmetic.
if (value < 0)
{
throw new DataErrorException();
}
_inputPosition++;
return value;
}
private void DecodeChunkHeader()
{
var control = _inputStream!.ReadByte();
_inputPosition++;
var control = ReadChunkHeaderByte();
if (control == 0x00)
{
@@ -539,17 +565,14 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
_uncompressedChunk = false;
_availableBytes = (control & 0x1F) << 16;
_availableBytes += (_inputStream.ReadByte() << 8) + _inputStream.ReadByte() + 1;
_inputPosition += 2;
_availableBytes += (ReadChunkHeaderByte() << 8) + ReadChunkHeaderByte() + 1;
_rangeDecoderLimit = (_inputStream.ReadByte() << 8) + _inputStream.ReadByte() + 1;
_inputPosition += 2;
_rangeDecoderLimit = (ReadChunkHeaderByte() << 8) + ReadChunkHeaderByte() + 1;
if (control >= 0xC0)
{
_needProps = false;
Properties[0] = (byte)_inputStream.ReadByte();
_inputPosition++;
Properties[0] = (byte)ReadChunkHeaderByte();
_decoder = new Decoder();
_decoder.SetDecoderProperties(Properties);
@@ -580,8 +603,7 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
else
{
_uncompressedChunk = true;
_availableBytes = (_inputStream.ReadByte() << 8) + _inputStream.ReadByte() + 1;
_inputPosition += 2;
_availableBytes = (ReadChunkHeaderByte() << 8) + ReadChunkHeaderByte() + 1;
}
}

View File

@@ -206,7 +206,8 @@ public class PpmdStream : Stream, IAsyncDisposable
_isDisposed = true;
if (_compress)
{
await _model.EncodeBlockAsync(_stream, new MemoryStream(), true).ConfigureAwait(false);
using var input = new PooledMemoryStream();
await _model.EncodeBlockAsync(_stream, input, true).ConfigureAwait(false);
}
_modelH?.Dispose();
_modelH = null;

View File

@@ -9,6 +9,7 @@ internal class BitStream
private int _bitIdx;
private int _bitsLeft;
private ulong _bitBuffer;
private bool _lastReadValid;
private static uint[] _maskBits = new uint[17]
{
0U,
@@ -62,6 +63,11 @@ internal class BitStream
_bitsLeft += 8;
}
}
_lastReadValid = _bitsLeft >= nbits;
if (!_lastReadValid)
{
return 0;
}
result = (int)(_bitBuffer & _maskBits[nbits]);
_bitBuffer >>= nbits;
_bitsLeft -= nbits;
@@ -70,10 +76,6 @@ internal class BitStream
public bool Advance(int count)
{
if (_byteIdx > _srcLen)
{
return false;
}
return true;
return _lastReadValid;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
namespace SharpCompress.Compressors.Shrink;
@@ -238,6 +239,63 @@ public class HwUnshrink
byte[] dst,
int dstCap,
out int dstUsed
) => UnshrinkCore(src, srcLen, out srcUsed, ref dst, dstCap, out dstUsed, null);
internal static UnshrnkStatus UnshrinkGrowing(
byte[] src,
int srcLen,
int dstCap,
ArrayPool<byte> pool,
out byte[] dst,
out int dstUsed
)
{
dst = Array.Empty<byte>();
try
{
return UnshrinkCore(src, srcLen, out _, ref dst, dstCap, out dstUsed, pool);
}
catch
{
if (dst.Length > 0)
{
pool.Return(dst);
dst = Array.Empty<byte>();
}
throw;
}
}
private static void ReserveOutput(ref byte[] dst, int dstPos, int dstCap, ArrayPool<byte>? pool)
{
if (pool is null)
{
return;
}
// A code string is bounded by the code table's ushort length. Reserve at most
// one string ahead of actual output, not the size advertised by the ZIP header.
var required = (int)Math.Min(dstCap, (long)dstPos + ushort.MaxValue + 1);
if (required > dst.Length)
{
var capacity = (int)Math.Min(dstCap, Math.Max(required, (long)dst.Length * 2));
var replacement = pool.Rent(capacity);
dst.AsSpan(0, dstPos).CopyTo(replacement);
if (dst.Length > 0)
{
pool.Return(dst);
}
dst = replacement;
}
}
private static UnshrnkStatus UnshrinkCore(
byte[] src,
int srcLen,
out int srcUsed,
ref byte[] dst,
int dstCap,
out int dstUsed,
ArrayPool<byte>? pool
)
{
var codeTab = new CodeTabEntry[HASHTAB_SIZE];
@@ -279,6 +337,7 @@ public class HwUnshrink
return UnshrnkStatus.Full;
}
ReserveOutput(ref dst, dstPos, dstCap, pool);
firstByte = (byte)currCode;
dst[dstPos] = (byte)currCode;
codeTab[currCode].lastDstPos = dstPos;
@@ -301,6 +360,7 @@ public class HwUnshrink
return UnshrnkStatus.Full;
}
ReserveOutput(ref dst, dstPos, dstCap, pool);
// Handle KwKwK: next code used before being added.
if (currCode == queue.codes[queue.nextIdx])
{

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Shrink;
@@ -28,15 +29,10 @@ internal partial class ShrinkStream : Stream
// 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();
using var srcMs = new PooledMemoryStream();
await _inStream.CopyToAsync(srcMs, 81920, cancellationToken).ConfigureAwait(false);
var src = srcMs.ToArray();
var srcLen = src.Length;
// Decompress synchronously (CPU-bound operation)
HwUnshrink.Unshrink(src, srcLen, out _, _byteOut, (int)_uncompressedSize, out var dstUsed);
_outBytesCount = dstUsed;
_decompressed = true;
// Decompress synchronously (CPU-bound operation).
Decode(srcMs);
}
public override async Task<int> ReadAsync(
@@ -46,6 +42,7 @@ internal partial class ShrinkStream : Stream
CancellationToken cancellationToken
)
{
ThrowIfDisposed();
cancellationToken.ThrowIfCancellationRequested();
if (!_decompressed)
@@ -72,6 +69,7 @@ internal partial class ShrinkStream : Stream
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
cancellationToken.ThrowIfCancellationRequested();
if (!_decompressed)

View File

@@ -1,22 +1,29 @@
using System;
using System.Buffers;
using System.IO;
using SharpCompress.Common;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Shrink;
internal partial class ShrinkStream : Stream
{
private readonly Stream _inStream;
private readonly ArrayPool<byte> _pool;
private readonly long _uncompressedSize;
private readonly byte[] _byteOut;
private byte[] _byteOut = Array.Empty<byte>();
private long _outBytesCount;
private bool _decompressed;
private bool _disposed;
private long _position;
public ShrinkStream(Stream stream, long uncompressedSize)
: this(stream, uncompressedSize, ArrayPool<byte>.Shared) { }
internal ShrinkStream(Stream stream, long uncompressedSize, ArrayPool<byte> pool)
{
if (uncompressedSize > int.MaxValue)
if (uncompressedSize < 0 || uncompressedSize > int.MaxValue)
{
throw new InvalidFormatException(
$"Shrink: declared uncompressed size {uncompressedSize} exceeds maximum supported size."
@@ -24,13 +31,13 @@ internal partial class ShrinkStream : Stream
}
_inStream = stream;
_pool = pool;
_uncompressedSize = uncompressedSize;
_byteOut = new byte[(int)_uncompressedSize];
_outBytesCount = 0L;
}
public override bool CanRead => true;
public override bool CanRead => !_disposed;
public override bool CanSeek => true;
@@ -48,27 +55,15 @@ internal partial class ShrinkStream : Stream
public override int Read(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
if (!_decompressed)
{
// 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();
using var srcMs = new PooledMemoryStream();
_inStream.CopyTo(srcMs);
var src = srcMs.ToArray();
var srcLen = src.Length;
HwUnshrink.Unshrink(
src,
srcLen,
out _,
_byteOut,
(int)_uncompressedSize,
out var dstUsed
);
_outBytesCount = dstUsed;
_decompressed = true;
_position = 0;
Decode(srcMs);
}
long remaining = _outBytesCount - _position;
@@ -86,6 +81,70 @@ internal partial class ShrinkStream : Stream
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotImplementedException();
private void Decode(PooledMemoryStream source)
{
var length = checked((int)source.Length);
var src = _pool.Rent(length);
var output = Array.Empty<byte>();
try
{
source.Position = 0;
if (source.Read(src, 0, length) != length)
{
throw new InvalidFormatException("Shrink: truncated compressed data.");
}
// A pool rental may be larger than the compressed data; never decode its unused tail.
var status = HwUnshrink.UnshrinkGrowing(
src,
length,
(int)_uncompressedSize,
_pool,
out output,
out var used
);
if (status != HwUnshrink.UnshrnkStatus.Ok || used != _uncompressedSize)
{
throw new InvalidFormatException("Shrink: invalid or truncated compressed data.");
}
_byteOut = output;
output = Array.Empty<byte>();
_outBytesCount = used;
_decompressed = true;
_position = 0;
}
finally
{
_pool.Return(src);
if (output.Length > 0)
{
_pool.Return(output);
}
}
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
var output = _byteOut;
_byteOut = Array.Empty<byte>();
if (output.Length > 0)
{
_pool.Return(output);
}
}
base.Dispose(disposing);
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ShrinkStream));
}
}
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) =>

View File

@@ -55,7 +55,7 @@ public partial class SevenZipWriter
var filesInfo = new SevenZipFilesInfoWriter { Entries = entries.ToArray() };
// Write header to a temporary stream first
using var headerStream = new MemoryStream();
using var headerStream = new PooledMemoryStream();
ArchiveHeaderWriter.WriteRawHeader(headerStream, mainStreamsInfo, filesInfo);
// Optionally compress the header
@@ -70,7 +70,7 @@ public partial class SevenZipWriter
}
private async ValueTask WriteCompressedHeaderAsync(
MemoryStream rawHeaderStream,
PooledMemoryStream rawHeaderStream,
long endOfPackedData
)
{
@@ -97,7 +97,7 @@ public partial class SevenZipWriter
};
// Write encoded header to a second temporary stream
using var encodedHeaderStream = new MemoryStream();
using var encodedHeaderStream = new PooledMemoryStream();
ArchiveHeaderWriter.WriteEncodedHeader(encodedHeaderStream, headerStreamsInfo);
// Write the encoded header to the output
@@ -125,7 +125,7 @@ public partial class SevenZipWriter
}
private async ValueTask WriteRawHeaderToOutputAsync(
MemoryStream rawHeaderStream,
PooledMemoryStream rawHeaderStream,
long endOfPackedData
)
{

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
namespace SharpCompress.Writers.Zip;
@@ -24,7 +25,7 @@ public partial class ZipWriter
// Buffer the entire central directory + end record into memory, then write async.
// This avoids synchronous writes to the underlying stream during finalization.
using var ms = new MemoryStream();
using var ms = new PooledMemoryStream();
ulong size = 0;
foreach (var entry in entries)
{
@@ -157,9 +158,9 @@ public partial class ZipWriter
CancellationToken cancellationToken
)
{
// Build the header synchronously into a MemoryStream, then async-copy to OutputStream.
// Build the header synchronously into a pooled stream, then async-copy to OutputStream.
// This avoids any synchronous writes to the potentially async-only output stream.
using var ms = new MemoryStream();
using var ms = new PooledMemoryStream();
var outputStream = OutputStream.NotNull();
var result = WriteHeader(
ms,