diff --git a/Directory.Build.props b/Directory.Build.props index 277c525f..6ff916b6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,5 +11,6 @@ true true true + ${NoWarn};IDE0051 diff --git a/src/SharpCompress/Common/Constants.cs b/src/SharpCompress/Common/Constants.cs index 5fba5ed9..9812501d 100644 --- a/src/SharpCompress/Common/Constants.cs +++ b/src/SharpCompress/Common/Constants.cs @@ -22,7 +22,9 @@ public static class Constants /// by rewinding and re-reading the same data. /// /// - /// Default: 81920 bytes (81KB) - sufficient for typical format detection. + /// Default: 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. /// /// /// Typical usage: 500-1000 bytes for most archives @@ -39,7 +41,7 @@ public static class Constants /// /// /// - public static int RewindableBufferSize { get; set; } = 81920; + public static int RewindableBufferSize { get; set; } = 163840; public static CultureInfo DefaultCultureInfo { get; set; } = CultureInfo.InvariantCulture; } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index 74be64fc..d68b8a87 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -86,55 +86,34 @@ internal partial class WinzipAesCryptoStream private void ReadTransformBlocks(Span buffer, int count) { var posn = 0; - var last = count; + var remaining = count; - while (posn < buffer.Length && posn < last) + while (posn < buffer.Length && remaining > 0) { - var n = ReadTransformOneBlock(buffer, posn, last); + var n = ReadTransformOneBlock(buffer, posn, remaining); posn += n; + remaining -= n; } } - private int ReadTransformOneBlock(Span buffer, int offset, int last) + private int ReadTransformOneBlock(Span buffer, int offset, int remaining) { - if (_isFinalBlock) + if (_counterOutOffset == BLOCK_SIZE_IN_BYTES) { - throw new ArchiveOperationException(); + FillCounterOut(); } - var bytesRemaining = last - offset; - var bytesToRead = - (bytesRemaining > BLOCK_SIZE_IN_BYTES) ? BLOCK_SIZE_IN_BYTES : bytesRemaining; - - // update the counter - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++); - - // Determine if this is the final block - if ((bytesToRead == bytesRemaining) && (_totalBytesLeftToRead == 0)) - { - _counterOut = _transform.TransformFinalBlock(_counter, 0, BLOCK_SIZE_IN_BYTES); - _isFinalBlock = true; - } - else - { - _transform.TransformBlock( - _counter, - 0, // offset - BLOCK_SIZE_IN_BYTES, - _counterOut, - 0 - ); // offset - } - - XorInPlace(buffer, offset, bytesToRead); - return bytesToRead; + var bytesToXor = Math.Min(BLOCK_SIZE_IN_BYTES - _counterOutOffset, remaining); + XorInPlace(buffer, offset, bytesToXor, _counterOutOffset); + _counterOutOffset += bytesToXor; + return bytesToXor; } - private void XorInPlace(Span buffer, int offset, int count) + private void XorInPlace(Span buffer, int offset, int count, int counterOffset) { for (var i = 0; i < count; i++) { - buffer[offset + i] = (byte)(_counterOut[i] ^ buffer[offset + i]); + buffer[offset + i] = (byte)(_counterOut[counterOffset + i] ^ buffer[offset + i]); } } #endif diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 2ea1d2d7..7d9cb753 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -16,7 +16,7 @@ internal partial class WinzipAesCryptoStream : Stream private readonly ICryptoTransform _transform; private int _nonce = 1; private byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES]; - private bool _isFinalBlock; + private int _counterOutOffset = BLOCK_SIZE_IN_BYTES; private long _totalBytesLeftToRead; private bool _isDisposed; @@ -123,58 +123,45 @@ internal partial class WinzipAesCryptoStream : Stream return read; } - private int ReadTransformOneBlock(byte[] buffer, int offset, int last) + private void FillCounterOut() { - if (_isFinalBlock) - { - throw new ArchiveOperationException(); - } - - var bytesRemaining = last - offset; - var bytesToRead = - (bytesRemaining > BLOCK_SIZE_IN_BYTES) ? BLOCK_SIZE_IN_BYTES : bytesRemaining; - // update the counter BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++); - - // Determine if this is the final block - if ((bytesToRead == bytesRemaining) && (_totalBytesLeftToRead == 0)) - { - _counterOut = _transform.TransformFinalBlock(_counter, 0, BLOCK_SIZE_IN_BYTES); - _isFinalBlock = true; - } - else - { - _transform.TransformBlock( - _counter, - 0, // offset - BLOCK_SIZE_IN_BYTES, - _counterOut, - 0 - ); // offset - } - - XorInPlace(buffer, offset, bytesToRead); - return bytesToRead; + _transform.TransformBlock( + _counter, + 0, // offset + BLOCK_SIZE_IN_BYTES, + _counterOut, + 0 + ); // offset + _counterOutOffset = 0; } - private void XorInPlace(byte[] buffer, int offset, int count) + private void XorInPlace(byte[] buffer, int offset, int count, int counterOffset) { for (var i = 0; i < count; i++) { - buffer[offset + i] = (byte)(_counterOut[i] ^ buffer[offset + i]); + buffer[offset + i] = (byte)(_counterOut[counterOffset + i] ^ buffer[offset + i]); } } private void ReadTransformBlocks(byte[] buffer, int offset, int count) { var posn = offset; - var last = count + offset; + var remaining = count; - while (posn < buffer.Length && posn < last) + while (posn < buffer.Length && remaining > 0) { - var n = ReadTransformOneBlock(buffer, posn, last); - posn += n; + if (_counterOutOffset == BLOCK_SIZE_IN_BYTES) + { + FillCounterOut(); + } + + var bytesToXor = Math.Min(BLOCK_SIZE_IN_BYTES - _counterOutOffset, remaining); + XorInPlace(buffer, posn, bytesToXor, _counterOutOffset); + _counterOutOffset += bytesToXor; + posn += bytesToXor; + remaining -= bytesToXor; } } diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs index 3c60e709..29f523a6 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs @@ -48,10 +48,8 @@ internal abstract partial class ZipFilePart } if ( - ( - Header.CompressedSize == 0 - && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) - ) || Header.IsZip64 + Header.CompressedSize == 0 + && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) ) { plainStream = SharpCompressStream.CreateNonDisposing(plainStream); //make sure AES doesn't close diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index e4ac519c..de182f37 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -234,10 +234,8 @@ internal abstract partial class ZipFilePart : FilePart } if ( - ( - Header.CompressedSize == 0 - && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) - ) || Header.IsZip64 + Header.CompressedSize == 0 + && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) ) { plainStream = SharpCompressStream.CreateNonDisposing(plainStream); //make sure AES doesn't close diff --git a/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs b/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs index 822b2d64..542d991a 100644 --- a/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs +++ b/src/SharpCompress/Compressors/ArcLzw/ArcLzwStream.cs @@ -73,6 +73,10 @@ public partial class ArcLzwStream : Stream if (useCrunched) { + if (input.Length == 0) + { + throw new InvalidFormatException("ArcLzwStream: compressed data is empty"); + } if (input[0] != BITS) { throw new InvalidFormatException($"File packed with {input[0]}, expected {BITS}."); @@ -129,6 +133,10 @@ public partial class ArcLzwStream : Stream while (code >= 256) { + if (code >= suffix.Length) + { + throw new InvalidFormatException("ArcLzwStream: code out of range"); + } stack.Push(suffix[code]); code = prefix[code]; } diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs index 75f50896..d148df50 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs @@ -237,6 +237,10 @@ internal partial class CBZip2InputStream /* Now the selectors */ nGroups = await BsRAsync(3, cancellationToken).ConfigureAwait(false); + if (nGroups < 2 || nGroups > BZip2Constants.N_GROUPS) + { + throw new InvalidFormatException("BZip2: invalid number of Huffman trees"); + } nSelectors = await BsRAsync(15, cancellationToken).ConfigureAwait(false); for (i = 0; i < nSelectors; i++) { @@ -244,6 +248,10 @@ internal partial class CBZip2InputStream while (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) { j++; + if (j >= nGroups) + { + throw new InvalidFormatException("BZip2: invalid selector MTF value"); + } } if (i < BZip2Constants.MAX_SELECTORS) { @@ -266,6 +274,10 @@ internal partial class CBZip2InputStream for (i = 0; i < nSelectors; i++) { v = selectorMtf[i]; + if (v >= nGroups) + { + throw new InvalidFormatException("BZip2: selector MTF value out of range"); + } tmp = pos[v]; while (v > 0) { @@ -374,6 +386,10 @@ internal partial class CBZip2InputStream while (zvec > limit[zt][zn]) { zn++; + if (zn >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: Huffman code too long"); + } { { while (bsLive < 1) @@ -405,7 +421,14 @@ internal partial class CBZip2InputStream } zvec = (zvec << 1) | zj; } - nextSym = perm[zt][zvec - basev[zt][zn]]; + { + int permIdx = zvec - basev[zt][zn]; + if (permIdx < 0 || permIdx >= perm[zt].Length) + { + throw new InvalidFormatException("BZip2: invalid Huffman symbol"); + } + nextSym = perm[zt][permIdx]; + } } while (true) @@ -448,6 +471,10 @@ internal partial class CBZip2InputStream while (zvec > limit[zt][zn]) { zn++; + if (zn >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: Huffman code too long"); + } { { while (bsLive < 1) @@ -479,7 +506,14 @@ internal partial class CBZip2InputStream } zvec = (zvec << 1) | zj; } - nextSym = perm[zt][zvec - basev[zt][zn]]; + { + int permIdx = zvec - basev[zt][zn]; + if (permIdx < 0 || permIdx >= perm[zt].Length) + { + throw new InvalidFormatException("BZip2: invalid Huffman symbol"); + } + nextSym = perm[zt][permIdx]; + } } } while (nextSym == BZip2Constants.RUNA || nextSym == BZip2Constants.RUNB); @@ -550,6 +584,10 @@ internal partial class CBZip2InputStream while (zvec > limit[zt][zn]) { zn++; + if (zn >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: Huffman code too long"); + } { { while (bsLive < 1) @@ -581,7 +619,14 @@ internal partial class CBZip2InputStream } zvec = (zvec << 1) | zj; } - nextSym = perm[zt][zvec - basev[zt][zn]]; + { + int permIdx = zvec - basev[zt][zn]; + if (permIdx < 0 || permIdx >= perm[zt].Length) + { + throw new InvalidFormatException("BZip2: invalid Huffman symbol"); + } + nextSym = perm[zt][permIdx]; + } } } } @@ -605,10 +650,18 @@ internal partial class CBZip2InputStream for (i = 0; i <= last; i++) { ch = ll8[i]; + if (cftab[ch] < 0 || cftab[ch] >= tt.Length) + { + throw new InvalidFormatException("BZip2: block data out of bounds"); + } tt[cftab[ch]] = i; cftab[ch]++; } + if (origPtr < 0 || origPtr >= tt.Length) + { + throw new InvalidFormatException("BZip2: origPtr out of bounds"); + } tPos = tt[origPtr]; count = 0; @@ -806,6 +859,10 @@ internal partial class CBZip2InputStream int v; while (bsLive < n) { + if (bsStream is null) + { + CompressedStreamEOF(); + } int zzi; int thech = '\0'; var b = ArrayPool.Shared.Rent(1); @@ -858,7 +915,10 @@ internal partial class CBZip2InputStream cbZip2InputStream.ll8 = null; cbZip2InputStream.tt = null; cbZip2InputStream.BsSetStream(zStream); - await cbZip2InputStream.InitializeAsync(true, cancellationToken).ConfigureAwait(false); + if (!await cbZip2InputStream.InitializeAsync(true, cancellationToken).ConfigureAwait(false)) + { + throw new InvalidFormatException("Not a valid BZip2 stream"); + } await cbZip2InputStream.InitBlockAsync(cancellationToken).ConfigureAwait(false); await cbZip2InputStream.SetupBlockAsync(cancellationToken).ConfigureAwait(false); return cbZip2InputStream; diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index 2fb58d21..053d1679 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -179,7 +179,10 @@ internal partial class CBZip2InputStream : Stream cbZip2InputStream.ll8 = null; cbZip2InputStream.tt = null; cbZip2InputStream.BsSetStream(zStream); - cbZip2InputStream.Initialize(true); + if (!cbZip2InputStream.Initialize(true)) + { + throw new InvalidFormatException("Not a valid BZip2 stream"); + } cbZip2InputStream.InitBlock(); cbZip2InputStream.SetupBlock(); return cbZip2InputStream; @@ -403,6 +406,10 @@ internal partial class CBZip2InputStream : Stream int v; while (bsLive < n) { + if (bsStream is null) + { + CompressedStreamEOF(); + } int zzi; int thech = '\0'; try @@ -477,6 +484,10 @@ internal partial class CBZip2InputStream : Stream } for (i = 0; i < alphaSize; i++) { + if (length[i] >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: invalid Huffman code length"); + } basev[length[i] + 1]++; } @@ -553,6 +564,10 @@ internal partial class CBZip2InputStream : Stream /* Now the selectors */ nGroups = BsR(3); + if (nGroups < 2 || nGroups > BZip2Constants.N_GROUPS) + { + throw new InvalidFormatException("BZip2: invalid number of Huffman trees"); + } nSelectors = BsR(15); for (i = 0; i < nSelectors; i++) { @@ -560,6 +575,10 @@ internal partial class CBZip2InputStream : Stream while (BsR(1) == 1) { j++; + if (j >= nGroups) + { + throw new InvalidFormatException("BZip2: invalid selector MTF value"); + } } if (i < BZip2Constants.MAX_SELECTORS) { @@ -582,6 +601,10 @@ internal partial class CBZip2InputStream : Stream for (i = 0; i < nSelectors; i++) { v = selectorMtf[i]; + if (v >= nGroups) + { + throw new InvalidFormatException("BZip2: selector MTF value out of range"); + } tmp = pos[v]; while (v > 0) { @@ -689,6 +712,10 @@ internal partial class CBZip2InputStream : Stream while (zvec > limit[zt][zn]) { zn++; + if (zn >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: Huffman code too long"); + } { { while (bsLive < 1) @@ -717,7 +744,14 @@ internal partial class CBZip2InputStream : Stream } zvec = (zvec << 1) | zj; } - nextSym = perm[zt][zvec - basev[zt][zn]]; + { + int permIdx = zvec - basev[zt][zn]; + if (permIdx < 0 || permIdx >= perm[zt].Length) + { + throw new InvalidFormatException("BZip2: invalid Huffman symbol"); + } + nextSym = perm[zt][permIdx]; + } } while (true) @@ -760,6 +794,10 @@ internal partial class CBZip2InputStream : Stream while (zvec > limit[zt][zn]) { zn++; + if (zn >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: Huffman code too long"); + } { { while (bsLive < 1) @@ -788,7 +826,14 @@ internal partial class CBZip2InputStream : Stream } zvec = (zvec << 1) | zj; } - nextSym = perm[zt][zvec - basev[zt][zn]]; + { + int permIdx = zvec - basev[zt][zn]; + if (permIdx < 0 || permIdx >= perm[zt].Length) + { + throw new InvalidFormatException("BZip2: invalid Huffman symbol"); + } + nextSym = perm[zt][permIdx]; + } } } while (nextSym == BZip2Constants.RUNA || nextSym == BZip2Constants.RUNB); @@ -859,6 +904,10 @@ internal partial class CBZip2InputStream : Stream while (zvec > limit[zt][zn]) { zn++; + if (zn >= BZip2Constants.MAX_CODE_LEN) + { + throw new InvalidFormatException("BZip2: Huffman code too long"); + } { { while (bsLive < 1) @@ -883,7 +932,14 @@ internal partial class CBZip2InputStream : Stream } zvec = (zvec << 1) | zj; } - nextSym = perm[zt][zvec - basev[zt][zn]]; + { + int permIdx = zvec - basev[zt][zn]; + if (permIdx < 0 || permIdx >= perm[zt].Length) + { + throw new InvalidFormatException("BZip2: invalid Huffman symbol"); + } + nextSym = perm[zt][permIdx]; + } } } } @@ -907,10 +963,18 @@ internal partial class CBZip2InputStream : Stream for (i = 0; i <= last; i++) { ch = ll8[i]; + if (cftab[ch] < 0 || cftab[ch] >= tt.Length) + { + throw new InvalidFormatException("BZip2: block data out of bounds"); + } tt[cftab[ch]] = i; cftab[ch]++; } + if (origPtr < 0 || origPtr >= tt.Length) + { + throw new InvalidFormatException("BZip2: origPtr out of bounds"); + } tPos = tt[origPtr]; count = 0; diff --git a/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs b/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs index e98cfc69..3a7b840c 100644 --- a/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs +++ b/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs @@ -208,6 +208,10 @@ internal sealed class HuffmanTree do { + if (index < 0 || index >= array.Length) + { + throw new InvalidFormatException("Deflate64: invalid Huffman data"); + } var value = array[index]; if (value == 0) diff --git a/src/SharpCompress/Compressors/Explode/ExplodeStream.Async.cs b/src/SharpCompress/Compressors/Explode/ExplodeStream.Async.cs index bd058dd4..6dcf2af3 100644 --- a/src/SharpCompress/Compressors/Explode/ExplodeStream.Async.cs +++ b/src/SharpCompress/Compressors/Explode/ExplodeStream.Async.cs @@ -20,7 +20,10 @@ public partial class ExplodeStream ) { var ex = new ExplodeStream(inStr, compressedSize, uncompressedSize, generalPurposeBitFlag); - await ex.explode_SetTables_async(cancellationToken).ConfigureAwait(false); + if (await ex.explode_SetTables_async(cancellationToken).ConfigureAwait(false) != 0) + { + throw new InvalidFormatException("ExplodeStream: invalid Huffman table data"); + } ex.explode_var_init(); return ex; } diff --git a/src/SharpCompress/Compressors/Explode/ExplodeStream.cs b/src/SharpCompress/Compressors/Explode/ExplodeStream.cs index d161f6a8..9ca53c8b 100644 --- a/src/SharpCompress/Compressors/Explode/ExplodeStream.cs +++ b/src/SharpCompress/Compressors/Explode/ExplodeStream.cs @@ -61,7 +61,10 @@ public partial class ExplodeStream : Stream ) { var ex = new ExplodeStream(inStr, compressedSize, uncompressedSize, generalPurposeBitFlag); - ex.explode_SetTables(); + if (ex.explode_SetTables() != 0) + { + throw new InvalidFormatException("ExplodeStream: invalid Huffman table data"); + } ex.explode_var_init(); return ex; } diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index df5a74b7..78300446 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; namespace SharpCompress.Compressors.LZMA.LZ; @@ -25,6 +26,10 @@ internal partial class OutWindow : IDisposable public void Create(int windowSize) { + if (windowSize <= 0) + { + throw new InvalidFormatException($"LZMA: invalid dictionary size {windowSize}"); + } if (_windowSize != windowSize) { if (_buffer is not null) diff --git a/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs b/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs index 5a8ac76b..4f6bbf40 100644 --- a/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs +++ b/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs @@ -70,7 +70,15 @@ public partial class LzwStream { if (!headerParsed) { - await ParseHeaderAsync(cancellationToken).ConfigureAwait(false); + try + { + await ParseHeaderAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + eof = true; + throw; + } } if (eof) @@ -348,6 +356,17 @@ public partial class LzwStream ); } + if (maxBits < LzwConstants.INIT_BITS) + { + throw new InvalidFormatException( + "Stream compressed with " + + maxBits + + " bits, but minimum supported is " + + LzwConstants.INIT_BITS + + " bits." + ); + } + if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0) { throw new ArchiveException("Unsupported bits set in the header."); diff --git a/src/SharpCompress/Compressors/Lzw/LzwStream.cs b/src/SharpCompress/Compressors/Lzw/LzwStream.cs index 277dfb77..0aa47222 100644 --- a/src/SharpCompress/Compressors/Lzw/LzwStream.cs +++ b/src/SharpCompress/Compressors/Lzw/LzwStream.cs @@ -129,7 +129,15 @@ public partial class LzwStream : Stream { if (!headerParsed) { - ParseHeader(); + try + { + ParseHeader(); + } + catch + { + eof = true; + throw; + } } if (eof) @@ -421,6 +429,17 @@ public partial class LzwStream : Stream ); } + if (maxBits < LzwConstants.INIT_BITS) + { + throw new InvalidFormatException( + "Stream compressed with " + + maxBits + + " bits, but minimum supported is " + + LzwConstants.INIT_BITS + + " bits." + ); + } + if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0) { throw new ArchiveException("Unsupported bits set in the header."); diff --git a/src/SharpCompress/Compressors/PPMd/I1/Model.cs b/src/SharpCompress/Compressors/PPMd/I1/Model.cs index 7b32fc09..6d0fc656 100644 --- a/src/SharpCompress/Compressors/PPMd/I1/Model.cs +++ b/src/SharpCompress/Compressors/PPMd/I1/Model.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; // This is a port of Dmitry Shkarin's PPMd Variant I Revision 1. // Ported by Michael Bone (mjbone03@yahoo.com.au). @@ -253,6 +254,10 @@ internal partial class Model _coder.RangeDecoderInitialize(source); StartModel(properties.ModelOrder, properties.RestorationMethod); _minimumContext = _maximumContext; + if (_minimumContext == PpmContext.ZERO) + { + throw new InvalidFormatException("PPMd: model context not initialized"); + } _numberStatistics = _minimumContext.NumberStatistics; return _coder; } @@ -268,6 +273,10 @@ internal partial class Model await _coder.RangeDecoderInitializeAsync(source, cancellationToken).ConfigureAwait(false); StartModel(properties.ModelOrder, properties.RestorationMethod); _minimumContext = _maximumContext; + if (_minimumContext == PpmContext.ZERO) + { + throw new InvalidFormatException("PPMd: model context not initialized"); + } _numberStatistics = _minimumContext.NumberStatistics; return _coder; } @@ -429,13 +438,16 @@ internal partial class Model if (modelOrder < 2) { _orderFall = _modelOrder; - for ( - var context = _maximumContext; - context.Suffix != PpmContext.ZERO; - context = context.Suffix - ) + if (_maximumContext != PpmContext.ZERO) { - _orderFall--; + for ( + var context = _maximumContext; + context.Suffix != PpmContext.ZERO; + context = context.Suffix + ) + { + _orderFall--; + } } return; } diff --git a/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs b/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs index 35248e51..5c4149b7 100644 --- a/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs +++ b/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; using SharpCompress.IO; namespace SharpCompress.Compressors.Reduce; @@ -96,6 +97,10 @@ public partial class ReduceStream cancellationToken ) .ConfigureAwait(false); + if (nextByteIndex >= nextByteTable[outByte].Length) + { + throw new InvalidFormatException("ReduceStream: next byte table index out of range"); + } outByte = nextByteTable[outByte][nextByteIndex]; return outByte; } diff --git a/src/SharpCompress/Compressors/Reduce/ReduceStream.cs b/src/SharpCompress/Compressors/Reduce/ReduceStream.cs index 2f976d0a..75de965c 100644 --- a/src/SharpCompress/Compressors/Reduce/ReduceStream.cs +++ b/src/SharpCompress/Compressors/Reduce/ReduceStream.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using SharpCompress.Common; namespace SharpCompress.Compressors.Reduce; @@ -192,6 +193,10 @@ public partial class ReduceStream : Stream return outByte; } READBITS(bitCountTable[nextByteTable[outByte].Length], out byte nextByteIndex); + if (nextByteIndex >= nextByteTable[outByte].Length) + { + throw new InvalidFormatException("ReduceStream: next byte table index out of range"); + } outByte = nextByteTable[outByte][nextByteIndex]; return outByte; } diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs index 11610d5e..d8090dca 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs @@ -99,6 +99,10 @@ public partial class SqueezeStream huffmanDecoded.WriteByte((byte)i); i = 0; } + else if (i >= numnodes) + { + throw new InvalidFormatException("SqueezeStream: invalid Huffman tree node index"); + } } huffmanDecoded.Position = 0; diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs index f9c4439c..09aa8b2c 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using SharpCompress.Common; using SharpCompress.Compressors.RLE90; namespace SharpCompress.Compressors.Squeezed; @@ -93,6 +94,10 @@ public partial class SqueezeStream : Stream huffmanDecoded.WriteByte((byte)i); i = 0; } + else if (i >= numnodes) + { + throw new InvalidFormatException("SqueezeStream: invalid Huffman tree node index"); + } } huffmanDecoded.Position = 0; diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index dd384d2d..6a063611 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -76,7 +76,8 @@ public sealed record ReaderOptions : IReaderOptions /// by rewinding and re-reading the same data. /// /// - /// Default: Constants.RewindableBufferSize (81920 bytes / 81KB) + /// Default: Constants.RewindableBufferSize (163840 bytes / 160KB) - sized to cover + /// ZStandard's worst-case first block on a tar archive (~131KB including header overhead). /// /// /// Typical usage: 500-1000 bytes for most archives diff --git a/tests/SharpCompress.Test/MalformedInputTests.cs b/tests/SharpCompress.Test/MalformedInputTests.cs new file mode 100644 index 00000000..56cf5d05 --- /dev/null +++ b/tests/SharpCompress.Test/MalformedInputTests.cs @@ -0,0 +1,134 @@ +#if !LEGACY_DOTNET +using System; +using System.IO; +using AwesomeAssertions; +using SharpCompress.Common; +using SharpCompress.Readers; +using Xunit; + +namespace SharpCompress.Test; + +/// +/// Tests that malformed compressed input is handled gracefully, throwing library exceptions +/// rather than unhandled IndexOutOfRangeException, DivideByZeroException, or NullReferenceException. +/// +public class MalformedInputTests +{ + private static void VerifyMalformedInputThrowsLibraryException(string hex) + { + var data = Convert.FromHexString(hex); + using var ms = new MemoryStream(data); + var buf = new byte[4096]; + + Action act = () => + { + using var reader = ReaderFactory.OpenReader(ms); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + using var entryStream = reader.OpenEntryStream(); + while (entryStream.Read(buf, 0, buf.Length) > 0) { } + } + } + }; + + act.Should() + .Throw() + .And.Should() + .BeAssignableTo( + "malformed input should throw a library exception, not a raw system exception" + ); + } + + [Fact] + public void LzwStream_DivideByZero_ThrowsLibraryException() + { + // LZW stream with invalid header that would cause DivideByZero on subsequent reads + VerifyMalformedInputThrowsLibraryException( + "1f9d1a362f20000000130003edd1310a8030f1605ca2b26245c47b97e6d615e29400000000130003edd1310a8030f1605c606060606060606060606060606060606060606060606060007f60606060280000" + ); + } + + [Fact] + public void LzwStream_IndexOutOfRange_ThrowsLibraryException() + { + // LZW stream with maxBits < INIT_BITS causing table size mismatch + VerifyMalformedInputThrowsLibraryException( + "1f9d0836e1553ac4e1ce9ea227000000000000001070b4058faf051127c54144f8bfe54192e141bab6efe8032c41cd64004aef53da4acc8077a5b26245c47b97e6d615e29400000000000003edd1310a8030f1e2ee66ff535d800000000b00000000" + ); + } + + [Fact] + public void BZip2_NullRef_InBsR_ThrowsLibraryException() + { + // BZip2 stream with invalid block size causing null bsStream access + VerifyMalformedInputThrowsLibraryException( + "425a6857575757575768575757575757fff2fff27c007159425a6857ff0f21007159c1e2d5e2" + ); + } + + [Fact] + public void BZip2_IndexOutOfRange_InGetAndMoveToFrontDecode_ThrowsLibraryException() + { + // BZip2 with malformed Huffman tables causing code-too-long or bad perm index + VerifyMalformedInputThrowsLibraryException( + "425a6839314159265359c1c080e2000001410000100244a000305a6839314159265359c1c080e2000001410000100244a00030cd00c3cd00c34629971772c080e2" + ); + } + + [Fact] + public void SqueezeStream_IndexOutOfRange_ThrowsLibraryException() + { + // Squeezed ARC stream with malformed Huffman tree node indices + VerifyMalformedInputThrowsLibraryException( + "1a041a425a081a0000090000606839425a081730765cbb311042265300040000090000606839425a081730765cbb31104226530053" + ); + } + + [Fact] + public void ArcLzwStream_IndexOutOfRange_ThrowsLibraryException() + { + // ARC LZW stream with empty or malformed compressed data + VerifyMalformedInputThrowsLibraryException( + "1a081a1931081a00000000f9ffffff00000000ddff000000000000000000000000000012006068394200000080c431b37fff531042d9ff" + ); + } + + [Fact] + public void ExplodeStream_IndexOutOfRange_ThrowsLibraryException() + { + // ZIP entry using Implode/Explode with invalid Huffman tables + VerifyMalformedInputThrowsLibraryException( + "504b03040a000000060000ff676767676767676767676767676700000000683a36060000676767676767676767676700000000000000000000000000000000000000000000000000000000630000000000800000000000002e7478745554090003a8c8b6696045ac6975780b000104e803000004e803000068656c6c6f0a504b01021e030a0000000000147f6f5c20303a3639314159265359c1c080e2000001410000100244a00030cd00c346299717786975870b000104e8030000780b000104e803000004e8030000504b050600000000010000e74f004040490000000064" + ); + } + + [Fact] + public void Deflate64_IndexOutOfRange_ThrowsLibraryException() + { + // ZIP entry using Deflate64 with invalid Huffman data + VerifyMalformedInputThrowsLibraryException( + "504b03040a00009709001c0068656c6c6f2e807874555409000000000000147f6f5c20303a36060000ff0600000009425a6839314159265359595959595959a481000000000000000000007478925554050001c601003dffff000000000000001e000000001e00000000000000000000e1490000000000" + ); + } + + [Fact] + public void PPMd_NullRef_ThrowsLibraryException() + { + // ZIP entry using PPMd with malformed properties triggering uninitialized model access + VerifyMalformedInputThrowsLibraryException( + "504b03040000007462001c905c206600fa80ffffffffff1f8b0a00000000000003edd1310a80cf0c00090010000b000000e000000000030000002e000000686515e294362f763ac439d493d62a3671081e05c14114b4058faf051127c54144f8bfe541ace141bab6ef643c2ce2000001410000100244a00040cd41bdc76c4aef3977a5b25645c47b97e6d615e294362f763ac439d493d62a367108f1e2ee66ff535efa7f3015e2943601003ac439d493d62a3671081e05c14114b4058faf3a0003edd1310a80cf8597e6d60500140409" + ); + } + + [Fact] + public void LZMA_NullRef_ThrowsLibraryException() + { + // ZIP entry using LZMA with invalid dictionary size (0) causing null window buffer access + VerifyMalformedInputThrowsLibraryException( + "504b03040a0200000e001c0068646c6c6f2e7478745554ac507578000000000000000000000000000000000000000000e80300000000000068030a0000000000147f040020303a360600002e7478745554090003a8c8b6696045ac69f5780b0006ff1d000908180000e8030000000000a4810000109a9a9a8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b9a0000000000000000000000e80300000000000068030a0000009a9a9a504b03440a6fcb486c6c6f2e74ffff" + ); + } +} +#endif diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs new file mode 100644 index 00000000..578f6212 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs @@ -0,0 +1,257 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using SharpCompress.Common.Zip; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class WinzipAesCryptoStreamTests +{ + [Fact] + public void Read_Decrypts_Data_For_Aligned_Buffer_Size() + { + const string password = "sample-password"; + byte[] plainText = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray(); + byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]; + using var stream = CreateStream(plainText, password, salt); + + byte[] actual = new byte[plainText.Length]; + int bytesRead = stream.Read(actual, 0, actual.Length); + + Assert.Equal(plainText.Length, bytesRead); + Assert.Equal(plainText, actual); + } + + [Fact] + public void Read_Preserves_Keystream_Between_NonAligned_Reads() + { + const string password = "sample-password"; + byte[] plainText = Enumerable.Range(0, 97).Select(i => (byte)i).ToArray(); + byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]; + using var stream = CreateStream(plainText, password, salt); + + byte[] actual = ReadWithChunkPattern( + (buffer, offset, count) => stream.Read(buffer, offset, count), + plainText.Length, + [13, 5, 29, 7, 43] + ); + + Assert.Equal(plainText, actual); + } + + [Fact] + public async Task ReadAsync_Preserves_Keystream_Between_NonAligned_Reads() + { + const string password = "sample-password"; + byte[] plainText = Enumerable + .Range(0, 113) + .Select(i => unchecked((byte)(255 - i))) + .ToArray(); + byte[] salt = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28]; + using var stream = CreateStream(plainText, password, salt); + + byte[] actual = await ReadWithChunkPatternAsync( + (buffer, offset, count) => stream.ReadAsync(buffer, offset, count), + plainText.Length, + [11, 3, 17, 5, 41] + ); + + Assert.Equal(plainText, actual); + } + + [Fact] + public async Task ReadAsync_Memory_Preserves_Keystream_Between_NonAligned_Reads() + { + const string password = "sample-password"; + byte[] plainText = Enumerable + .Range(0, 113) + .Select(i => unchecked((byte)(255 - i))) + .ToArray(); + byte[] salt = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28]; + using var stream = CreateStream(plainText, password, salt); + + byte[] actual = await ReadWithChunkPatternMemoryAsync( + stream, + plainText.Length, + [11, 3, 17, 5, 41] + ); + + Assert.Equal(plainText, actual); + } + + [Fact] + public void Read_Stops_At_Encrypted_Payload_Length() + { + const string password = "sample-password"; + byte[] plainText = Enumerable.Range(0, 31).Select(i => (byte)(i * 3)).ToArray(); + byte[] salt = [0xA1, 0xB2, 0xC3, 0xD4, 0x01, 0x12, 0x23, 0x34]; + using var stream = CreateStream(plainText, password, salt); + + byte[] actual = new byte[plainText.Length + 16]; + int bytesRead = stream.Read(actual, 0, actual.Length); + int eofRead = stream.Read(actual, bytesRead, actual.Length - bytesRead); + + Assert.Equal(plainText.Length, bytesRead); + Assert.Equal(0, eofRead); + Assert.Equal(plainText, actual.Take(bytesRead).ToArray()); + } + + private static WinzipAesCryptoStream CreateStream( + byte[] plainText, + string password, + byte[] salt + ) + { + var encryptionData = CreateEncryptionData(password, salt); + byte[] cipherText = EncryptCtr(plainText, encryptionData.KeyBytes); + byte[] archiveBytes = cipherText.Concat(new byte[10]).ToArray(); + return new WinzipAesCryptoStream( + new MemoryStream(archiveBytes, writable: false), + encryptionData, + cipherText.Length + ); + } + + [SuppressMessage( + "Security", + "CA5379:Rfc2898DeriveBytes might be using a weak hash algorithm", + Justification = "WinZip AES interop requires PBKDF2 with SHA-1." + )] + private static WinzipAesEncryptionData CreateEncryptionData(string password, byte[] salt) + { +#pragma warning disable SYSLIB0060 // Rfc2898DeriveBytes might be using a weak hash algorithm + using var deriveBytes = new Rfc2898DeriveBytes( + password, + salt, + 1000, + HashAlgorithmName.SHA1 + ); +#pragma warning restore SYSLIB0060 + deriveBytes.GetBytes(16); + deriveBytes.GetBytes(16); + byte[] passwordVerifyValue = deriveBytes.GetBytes(2); + + return new WinzipAesEncryptionData( + WinzipAesKeySize.KeySize128, + salt, + passwordVerifyValue, + password + ); + } + + private static byte[] EncryptCtr(byte[] plainText, byte[] keyBytes) + { + using var aes = Aes.Create(); + aes.BlockSize = 128; + aes.KeySize = keyBytes.Length * 8; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + + using var encryptor = aes.CreateEncryptor(keyBytes, new byte[16]); + byte[] counter = new byte[16]; + byte[] counterOut = new byte[16]; + byte[] cipherText = new byte[plainText.Length]; + int nonce = 1; + int offset = 0; + + while (offset < plainText.Length) + { + BinaryPrimitives.WriteInt32LittleEndian(counter, nonce++); + encryptor.TransformBlock(counter, 0, counter.Length, counterOut, 0); + + int blockLength = Math.Min(counterOut.Length, plainText.Length - offset); + for (int i = 0; i < blockLength; i++) + { + cipherText[offset + i] = (byte)(plainText[offset + i] ^ counterOut[i]); + } + + offset += blockLength; + } + + return cipherText; + } + + private static byte[] ReadWithChunkPattern( + Func read, + int totalLength, + int[] chunkPattern + ) + { + byte[] actual = new byte[totalLength]; + int offset = 0; + int chunkIndex = 0; + + while (offset < totalLength) + { + int requested = Math.Min( + chunkPattern[chunkIndex % chunkPattern.Length], + totalLength - offset + ); + int bytesRead = read(actual, offset, requested); + Assert.True(bytesRead > 0); + offset += bytesRead; + chunkIndex++; + } + + return actual; + } + + private static async Task ReadWithChunkPatternAsync( + Func> readAsync, + int totalLength, + int[] chunkPattern + ) + { + byte[] actual = new byte[totalLength]; + int offset = 0; + int chunkIndex = 0; + + while (offset < totalLength) + { + int requested = Math.Min( + chunkPattern[chunkIndex % chunkPattern.Length], + totalLength - offset + ); + int bytesRead = await readAsync(actual, offset, requested); + Assert.True(bytesRead > 0); + offset += bytesRead; + chunkIndex++; + } + + return actual; + } + + private static async Task ReadWithChunkPatternMemoryAsync( + Stream stream, + int totalLength, + int[] chunkPattern + ) + { + byte[] actual = new byte[totalLength]; + int offset = 0; + int chunkIndex = 0; + + while (offset < totalLength) + { + int requested = Math.Min( + chunkPattern[chunkIndex % chunkPattern.Length], + totalLength - offset + ); +#if NET48 + int bytesRead = await stream.ReadAsync(actual, offset, requested); +#else + int bytesRead = await stream.ReadAsync(actual.AsMemory(offset, requested)); +#endif + Assert.True(bytesRead > 0); + offset += bytesRead; + chunkIndex++; + } + + return actual; + } +} diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 914688c2..92073da4 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -15,6 +15,8 @@ namespace SharpCompress.Test.Zip; public class ZipArchiveTests : ArchiveTests { + private const long GeneratedZip64EntrySize = (long)uint.MaxValue + 1; + public ZipArchiveTests() => UseExtensionInsteadOfNameToVerify = true; [Fact] @@ -516,6 +518,53 @@ public class ZipArchiveTests : ArchiveTests } } + [Fact] + public void Zip_Zstandard_WinzipAES_Mixed_ArchiveFileRead() + { + using var archive = ZipArchive.OpenArchive( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.zstd.WinzipAES.mixed.zip"), + new ReaderOptions { Password = "test" } + ); + + VerifyMixedZstandardArchive(archive); + } + + [Fact] + public void Zip_Zstandard_WinzipAES_Mixed_ArchiveStreamRead() + { + using var stream = File.OpenRead( + Path.Combine(TEST_ARCHIVES_PATH, "Zip.zstd.WinzipAES.mixed.zip") + ); + using var archive = ZipArchive.OpenArchive(stream, new ReaderOptions { Password = "test" }); + + VerifyMixedZstandardArchive(archive); + } + + [Fact(Explicit = true)] + [Trait("zip64", "generated")] + public void Zip_Zip64_GeneratedArchive_StreamIsBoundedToEntryLength() + { + var zipPath = Path.Combine(SCRATCH2_FILES_PATH, "generated.zip64.large.zip"); + CreateZip64Archive(zipPath); + + using var archive = ZipArchive.OpenArchive(zipPath); + var entries = archive + .Entries.Where(x => !x.IsDirectory) + .OrderByDescending(x => x.Size) + .ToArray(); + + Assert.Equal(2, entries.Length); + Assert.Equal(GeneratedZip64EntrySize, entries[0].Size); + Assert.Equal(1, entries[1].Size); + + using var firstStream = entries[0].OpenEntryStream(); + Assert.Equal(entries[0].Size, CountBytes(firstStream)); + + using var secondStream = entries[1].OpenEntryStream(); + Assert.Equal(0x42, secondStream.ReadByte()); + Assert.Equal(-1, secondStream.ReadByte()); + } + [Fact] public void Zip_Pkware_CompressionType() { @@ -910,4 +959,89 @@ public class ZipArchiveTests : ArchiveTests entries[0].WriteTo(outStream); Assert.Equal(0, outStream.Length); } + + private static void VerifyMixedZstandardArchive(IArchive archive) + { + var entries = archive.Entries.Where(x => !x.IsDirectory).ToArray(); + Assert.Equal(4, entries.Length); + Assert.Equal(2, entries.Count(x => x.IsEncrypted)); + Assert.Equal( + [".signature", "encrypted-zstd-entry.bin", "plain-zstd-entry.bin", "tables.db"], + entries.Select(x => x.Key.NotNull()).OrderBy(x => x).ToArray() + ); + Assert.All( + entries, + entry => Assert.Equal(CompressionType.ZStandard, entry.CompressionType) + ); + + var expectedSizes = new long[] { 160, 64 * 1024, 64 * 1024, 192 * 1024 }; + Assert.Equal(expectedSizes, entries.Select(x => x.Size).OrderBy(x => x).ToArray()); + + foreach (var entry in entries) + { + using var entryStream = entry.OpenEntryStream(); + using var target = new MemoryStream(); + entryStream.CopyTo(target); + Assert.Equal(entry.Size, target.Length); + } + } + + private static long CountBytes(Stream stream) + { + var buffer = new byte[8 * 1024 * 1024]; + long total = 0; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + total += read; + } + + return total; + } + + private static void CreateZip64Archive(string path) + { + if (File.Exists(path)) + { + File.Delete(path); + } + + var writerOptions = new ZipWriterOptions(CompressionType.None) { UseZip64 = true }; + using var fileStream = File.OpenWrite(path); + using var zipWriter = (ZipWriter) + WriterFactory.OpenWriter(fileStream, ArchiveType.Zip, writerOptions); + + using ( + var largeEntryStream = zipWriter.WriteToStream( + "large-entry.bin", + new ZipWriterEntryOptions() + ) + ) + { + WriteZeroes(largeEntryStream, GeneratedZip64EntrySize); + } + + using ( + var trailingEntryStream = zipWriter.WriteToStream( + "trailing-entry.bin", + new ZipWriterEntryOptions() + ) + ) + { + trailingEntryStream.WriteByte(0x42); + } + } + + private static void WriteZeroes(Stream stream, long length) + { + byte[] buffer = new byte[8 * 1024 * 1024]; + long remaining = length; + + while (remaining > 0) + { + int chunk = (int)Math.Min(buffer.Length, remaining); + stream.Write(buffer, 0, chunk); + remaining -= chunk; + } + } } diff --git a/tests/SharpCompress.Test/Zip/ZipFilePartTests.cs b/tests/SharpCompress.Test/Zip/ZipFilePartTests.cs new file mode 100644 index 00000000..a299cfbf --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipFilePartTests.cs @@ -0,0 +1,59 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Common.Zip; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.IO; +using SharpCompress.Providers; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class ZipFilePartTests +{ + [Fact] + public void GetCryptoStream_Bounds_Known_Size_Zip64_Entries() + { + var header = new DirectoryEntryHeader(new ArchiveEncoding()) + { + Name = "entry.bin", + CompressionMethod = ZipCompressionMethod.None, + CompressedSize = uint.MaxValue, + UncompressedSize = uint.MaxValue, + }; + + using var backingStream = new MemoryStream([1, 2, 3, 4, 5], writable: false); + var part = new TestZipFilePart(header, backingStream); + + using var cryptoStream = part.OpenCryptoStream(); + + Assert.IsType(cryptoStream); + } + + [Fact] + public void GetCryptoStream_Leaves_DataDescriptor_Entries_Unbounded_When_Size_Is_Unknown() + { + var header = new DirectoryEntryHeader(new ArchiveEncoding()) + { + Name = "entry.bin", + CompressionMethod = ZipCompressionMethod.None, + CompressedSize = 0, + UncompressedSize = 0, + Flags = HeaderFlags.UsePostDataDescriptor, + }; + + using var backingStream = new MemoryStream([1, 2, 3, 4, 5], writable: false); + var part = new TestZipFilePart(header, backingStream); + + using var cryptoStream = part.OpenCryptoStream(); + + Assert.IsNotType(cryptoStream); + } + + private sealed class TestZipFilePart(ZipFileEntry header, Stream stream) + : ZipFilePart(header, stream, CompressionProviderRegistry.Default) + { + public Stream OpenCryptoStream() => GetCryptoStream(CreateBaseStream()); + + protected override Stream CreateBaseStream() => BaseStream; + } +} diff --git a/tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip b/tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip new file mode 100644 index 00000000..506e49c9 Binary files /dev/null and b/tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip differ