mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Merge commit from fork
Prevent excessive allocations from forged 7z header fields
This commit is contained in:
@@ -480,4 +480,6 @@ ARC LZW compressed data is buffered incrementally from the bytes actually presen
|
||||
|
||||
ZIP Shrink output buffering grows with decoded output rather than the declared uncompressed size. Invalid codes and output-size mismatches are rejected.
|
||||
|
||||
7z header counts and coder property lengths are validated against their backing metadata before allocation. File counts are checked against parsed substreams and empty-stream bitmaps, including external metadata. Encoded headers and additional metadata are buffered incrementally as the decoder produces bytes, rather than preallocating the declared unpacked size.
|
||||
|
||||
LZMA/LZMA2 dictionary storage starts small and grows with decoded history, up to the supported dictionary size. These safeguards avoid allocations based solely on forged sizes; they do not impose an overall decompressed-output or process-memory limit.
|
||||
|
||||
@@ -193,6 +193,7 @@ internal sealed partial class ArchiveReader
|
||||
var packIndex = 0;
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var unpackSize = GetMetadataSize(folder);
|
||||
var oldDataStartPos = dataStartPos;
|
||||
var myPackSizes = new long[folder._packStreams.Count];
|
||||
for (var i = 0; i < myPackSizes.Length; i++)
|
||||
@@ -202,7 +203,7 @@ internal sealed partial class ArchiveReader
|
||||
dataStartPos += packSize;
|
||||
}
|
||||
|
||||
var outStream = await DecoderStreamHelper
|
||||
using var outStream = await DecoderStreamHelper
|
||||
.CreateDecoderStreamAsync(
|
||||
_stream,
|
||||
oldDataStartPos,
|
||||
@@ -213,15 +214,29 @@ internal sealed partial class ArchiveReader
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var unpackSize = checked((int)folder.GetUnpackSize());
|
||||
var data = new byte[unpackSize];
|
||||
await outStream
|
||||
.ReadExactAsync(data, 0, data.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (outStream.ReadByte() >= 0)
|
||||
// Grow only as the decoder actually produces metadata, never
|
||||
// from the untrusted unpack-size declaration alone.
|
||||
using var decoded = new PooledMemoryStream();
|
||||
var buffer = new byte[8192];
|
||||
for (var remaining = unpackSize; remaining > 0; )
|
||||
{
|
||||
var count = Math.Min(buffer.Length, remaining);
|
||||
await outStream
|
||||
.ReadExactAsync(buffer, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await decoded
|
||||
.WriteAsync(buffer, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
remaining -= count;
|
||||
}
|
||||
if (
|
||||
await outStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false)
|
||||
!= 0
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Decoded stream is longer than expected.");
|
||||
}
|
||||
var data = decoded.ToArray();
|
||||
dataVector.Add(data);
|
||||
|
||||
if (folder.UnpackCrcDefined)
|
||||
@@ -319,6 +334,7 @@ internal sealed partial class ArchiveReader
|
||||
}
|
||||
|
||||
var numFiles = ReadNum();
|
||||
ValidateFileCount(numFiles, unpackSizes.Count);
|
||||
db._files = new List<CFileItem>(numFiles);
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
|
||||
@@ -92,6 +92,7 @@ internal partial class ArchiveReader
|
||||
|
||||
private BitVector ReadBitVector(int length)
|
||||
{
|
||||
_currentReader.RequireBytes(((long)length + 7) / 8);
|
||||
var bits = new BitVector(length);
|
||||
|
||||
byte data = 0;
|
||||
@@ -202,9 +203,16 @@ internal partial class ArchiveReader
|
||||
try
|
||||
{
|
||||
var numCoders = ReadNum();
|
||||
// CFolder.CheckStructure supports at most 32 coders/bind pairs, but runs
|
||||
// after parsing. Enforce those limits before allocating their lists.
|
||||
if (numCoders == 0 || numCoders > CFolder.MaxCoders)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid number of 7z coders.");
|
||||
}
|
||||
_currentReader.RequireBytes(numCoders);
|
||||
folder._coders = new List<CCoderInfo>(numCoders);
|
||||
var numInStreams = 0;
|
||||
var numOutStreams = 0;
|
||||
long numInStreamsTotal = 0;
|
||||
long numOutStreamsTotal = 0;
|
||||
for (var i = 0; i < numCoders; i++)
|
||||
{
|
||||
try
|
||||
@@ -241,6 +249,7 @@ internal partial class ArchiveReader
|
||||
if ((mainByte & 0x20) != 0)
|
||||
{
|
||||
var propsSize = ReadNum();
|
||||
_currentReader.RequireBytes(propsSize);
|
||||
coder._props = new byte[propsSize];
|
||||
ReadBytes(coder._props, 0, propsSize);
|
||||
}
|
||||
@@ -250,30 +259,48 @@ internal partial class ArchiveReader
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
numInStreams += coder._numInStreams;
|
||||
numOutStreams += coder._numOutStreams;
|
||||
if (coder._numInStreams == 0 || coder._numOutStreams == 0)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z coder stream count.");
|
||||
}
|
||||
numInStreamsTotal += coder._numInStreams;
|
||||
numOutStreamsTotal += coder._numOutStreams;
|
||||
}
|
||||
finally { }
|
||||
}
|
||||
|
||||
if (
|
||||
numOutStreamsTotal - 1 > CFolder.MaxBindPairs
|
||||
|| numInStreamsTotal > int.MaxValue
|
||||
|| numInStreamsTotal < numOutStreamsTotal
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z folder stream counts.");
|
||||
}
|
||||
var numInStreams = (int)numInStreamsTotal;
|
||||
var numOutStreams = (int)numOutStreamsTotal;
|
||||
var numBindPairs = numOutStreams - 1;
|
||||
_currentReader.RequireBytes(2L * numBindPairs);
|
||||
folder._bindPairs = new List<CBindPair>(numBindPairs);
|
||||
for (var i = 0; i < numBindPairs; i++)
|
||||
{
|
||||
var bp = new CBindPair();
|
||||
bp._inIndex = ReadNum();
|
||||
bp._outIndex = ReadNum();
|
||||
if (
|
||||
bp._inIndex >= numInStreams
|
||||
|| bp._outIndex >= numOutStreams
|
||||
|| folder.FindBindPairForInStream(bp._inIndex) >= 0
|
||||
|| folder.FindBindPairForOutStream(bp._outIndex) >= 0
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z bind pair.");
|
||||
}
|
||||
folder._bindPairs.Add(bp);
|
||||
}
|
||||
|
||||
if (numInStreams < numBindPairs)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var numPackStreams = numInStreams - numBindPairs;
|
||||
|
||||
//folder.PackStreams.Reserve(numPackStreams);
|
||||
if (numPackStreams == 1)
|
||||
{
|
||||
for (var i = 0; i < numInStreams; i++)
|
||||
@@ -292,9 +319,19 @@ internal partial class ArchiveReader
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentReader.RequireBytes(numPackStreams);
|
||||
var packStreams = new HashSet<int>();
|
||||
for (var i = 0; i < numPackStreams; i++)
|
||||
{
|
||||
var num = ReadNum();
|
||||
if (
|
||||
num >= numInStreams
|
||||
|| folder.FindBindPairForInStream(num) >= 0
|
||||
|| !packStreams.Add(num)
|
||||
)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z packed stream index.");
|
||||
}
|
||||
folder._packStreams.Add(num);
|
||||
}
|
||||
}
|
||||
@@ -304,7 +341,23 @@ internal partial class ArchiveReader
|
||||
|
||||
private List<uint?> ReadHashDigests(int count)
|
||||
{
|
||||
// An all-defined vector has no bitmap, but must have a CRC per item.
|
||||
// Check it before ReadOptionalBitVector allocates the implicit bitmap.
|
||||
var reader = _currentReader.Clone();
|
||||
if (reader.ReadByte() != 0)
|
||||
{
|
||||
reader.RequireBytes(4L * count);
|
||||
}
|
||||
var defined = ReadOptionalBitVector(count);
|
||||
long numDefined = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (defined[i])
|
||||
{
|
||||
numDefined++;
|
||||
}
|
||||
}
|
||||
_currentReader.RequireBytes(4 * numDefined);
|
||||
var digests = new List<uint?>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
@@ -336,6 +389,7 @@ internal partial class ArchiveReader
|
||||
var numPackStreams = ReadNum();
|
||||
|
||||
WaitAttribute(BlockType.Size);
|
||||
_currentReader.RequireBytes(numPackStreams);
|
||||
packSizes = new List<long>(numPackStreams);
|
||||
for (var i = 0; i < numPackStreams; i++)
|
||||
{
|
||||
@@ -382,8 +436,9 @@ internal partial class ArchiveReader
|
||||
{
|
||||
streamSwitch.Set(this, dataVector ?? []);
|
||||
|
||||
//folders.Clear();
|
||||
//folders.Reserve(numFolders);
|
||||
// Folder definitions may live in an external metadata stream.
|
||||
// Validate against the selected stream, not the outer header.
|
||||
_currentReader.RequireBytes(numFolders);
|
||||
folders = new List<CFolder>(numFolders);
|
||||
var index = 0;
|
||||
for (var i = 0; i < numFolders; i++)
|
||||
@@ -405,6 +460,10 @@ internal partial class ArchiveReader
|
||||
var size = checked((long)ReadNumber());
|
||||
folder._unpackSizes.Add(size);
|
||||
}
|
||||
if (!folder.CheckStructure())
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z folder structure.");
|
||||
}
|
||||
}
|
||||
|
||||
for (; ; )
|
||||
@@ -476,6 +535,21 @@ internal partial class ArchiveReader
|
||||
}
|
||||
}
|
||||
|
||||
// Every substream except the last in each folder needs an explicit
|
||||
// size. Reject impossible counts before loops or digest allocations.
|
||||
long numExplicitSizes = 0;
|
||||
long numStreamsTotal = 0;
|
||||
foreach (var count in numUnpackStreamsInFolders)
|
||||
{
|
||||
numExplicitSizes += Math.Max(0, count - 1);
|
||||
numStreamsTotal += count;
|
||||
}
|
||||
if (numStreamsTotal > int.MaxValue || (numExplicitSizes != 0 && type != BlockType.Size))
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z substream count.");
|
||||
}
|
||||
_currentReader.RequireBytes(numExplicitSizes);
|
||||
|
||||
unpackSizes = new List<long>(folders.Count);
|
||||
for (var i = 0; i < numUnpackStreamsInFolders.Count; i++)
|
||||
{
|
||||
@@ -492,6 +566,10 @@ internal partial class ArchiveReader
|
||||
if (type == BlockType.Size)
|
||||
{
|
||||
var size = checked((long)ReadNumber());
|
||||
if (size > folders[i].GetUnpackSize() - sum)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z substream size.");
|
||||
}
|
||||
unpackSizes.Add(size);
|
||||
sum += size;
|
||||
}
|
||||
@@ -620,6 +698,67 @@ internal partial class ArchiveReader
|
||||
finally { }
|
||||
}
|
||||
|
||||
private static int GetMetadataSize(CFolder folder)
|
||||
{
|
||||
var size = folder.GetUnpackSize();
|
||||
if (size < 0 || size > int.MaxValue)
|
||||
{
|
||||
throw new InvalidFormatException("7z metadata is too large.");
|
||||
}
|
||||
return (int)size;
|
||||
}
|
||||
|
||||
private void ValidateFileCount(int numFiles, int numStreams)
|
||||
{
|
||||
// Nonempty files correspond to already parsed substreams. Empty files
|
||||
// require one bit each in kEmptyStream, not one byte per file. Preflight
|
||||
// that property before allocating CFileItem objects or implicit vectors.
|
||||
var reader = _currentReader.Clone();
|
||||
var numEmptyStreams = 0;
|
||||
var hasEmptyStreams = false;
|
||||
for (; ; )
|
||||
{
|
||||
var type = reader.ReadNumber();
|
||||
if (type == (ulong)BlockType.End)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var size = reader.ReadNumber();
|
||||
if (size > (ulong)reader.Remaining)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
var start = reader.Offset;
|
||||
if (type == (ulong)BlockType.EmptyStream)
|
||||
{
|
||||
var bitmapSize = ((long)numFiles + 7) / 8;
|
||||
if (hasEmptyStreams || size < (ulong)bitmapSize)
|
||||
{
|
||||
throw new InvalidFormatException("Invalid 7z empty stream bitmap.");
|
||||
}
|
||||
hasEmptyStreams = true;
|
||||
byte bits = 0;
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
if ((i & 7) == 0)
|
||||
{
|
||||
bits = reader.ReadByte();
|
||||
}
|
||||
if ((bits & 0x80) != 0)
|
||||
{
|
||||
numEmptyStreams++;
|
||||
}
|
||||
bits <<= 1;
|
||||
}
|
||||
}
|
||||
reader.SkipData((long)size - (reader.Offset - start));
|
||||
}
|
||||
if ((long)numStreams + numEmptyStreams != numFiles)
|
||||
{
|
||||
throw new InvalidFormatException("7z file count does not match the stream count.");
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> ReadAndDecodePackedStreams(long baseOffset, IPasswordProvider pass)
|
||||
{
|
||||
try
|
||||
@@ -641,6 +780,7 @@ internal partial class ArchiveReader
|
||||
var packIndex = 0;
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var unpackSize = GetMetadataSize(folder);
|
||||
var oldDataStartPos = dataStartPos;
|
||||
var myPackSizes = new long[folder._packStreams.Count];
|
||||
for (var i = 0; i < myPackSizes.Length; i++)
|
||||
@@ -650,7 +790,7 @@ internal partial class ArchiveReader
|
||||
dataStartPos += packSize;
|
||||
}
|
||||
|
||||
var outStream = DecoderStreamHelper.CreateDecoderStream(
|
||||
using var outStream = DecoderStreamHelper.CreateDecoderStream(
|
||||
_stream,
|
||||
oldDataStartPos,
|
||||
myPackSizes,
|
||||
@@ -658,13 +798,22 @@ internal partial class ArchiveReader
|
||||
pass
|
||||
);
|
||||
|
||||
var unpackSize = checked((int)folder.GetUnpackSize());
|
||||
var data = new byte[unpackSize];
|
||||
outStream.ReadExact(data, 0, data.Length);
|
||||
// Do not reserve the declared unpack size: malformed compressed
|
||||
// metadata can advertise GiB of output without producing a byte.
|
||||
using var decoded = new PooledMemoryStream();
|
||||
var buffer = new byte[8192];
|
||||
for (var remaining = unpackSize; remaining > 0; )
|
||||
{
|
||||
var count = Math.Min(buffer.Length, remaining);
|
||||
outStream.ReadExact(buffer, 0, count);
|
||||
decoded.Write(buffer, 0, count);
|
||||
remaining -= count;
|
||||
}
|
||||
if (outStream.ReadByte() >= 0)
|
||||
{
|
||||
throw new InvalidFormatException("Decoded stream is longer than expected.");
|
||||
}
|
||||
var data = decoded.ToArray();
|
||||
dataVector.Add(data);
|
||||
|
||||
if (folder.UnpackCrcDefined)
|
||||
@@ -756,6 +905,7 @@ internal partial class ArchiveReader
|
||||
}
|
||||
|
||||
var numFiles = ReadNum();
|
||||
ValidateFileCount(numFiles, unpackSizes.Count);
|
||||
db._files = new List<CFileItem>(numFiles);
|
||||
for (var i = 0; i < numFiles; i++)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace SharpCompress.Common.SevenZip;
|
||||
|
||||
internal class CFolder
|
||||
{
|
||||
internal const int MaxCoders = 32;
|
||||
internal const int MaxBindPairs = 32;
|
||||
|
||||
internal List<CCoderInfo> _coders = new();
|
||||
internal List<CBindPair> _bindPairs = new();
|
||||
internal List<int> _packStreams = new();
|
||||
@@ -98,11 +101,9 @@ internal class CFolder
|
||||
|
||||
public bool CheckStructure()
|
||||
{
|
||||
const int kNumCodersMax = 32; // don't change it
|
||||
const int kMaskSize = 32; // it must be >= kNumCodersMax
|
||||
const int kNumBindsMax = 32;
|
||||
const int kMaskSize = 32; // it must be >= MaxCoders
|
||||
|
||||
if (_coders.Count > kNumCodersMax || _bindPairs.Count > kNumBindsMax)
|
||||
if (_coders.Count > MaxCoders || _bindPairs.Count > MaxBindPairs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,18 @@ internal class DataReader
|
||||
|
||||
public int Offset { get; private set; }
|
||||
|
||||
public int Remaining => _ending - Offset;
|
||||
|
||||
public DataReader Clone() => new(_buffer, Offset, Remaining);
|
||||
|
||||
public void RequireBytes(long count)
|
||||
{
|
||||
if (count < 0 || count > Remaining)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
if (Offset >= _ending)
|
||||
@@ -57,10 +69,7 @@ internal class DataReader
|
||||
|
||||
public void ReadBytes(byte[] buffer, int offset, int length)
|
||||
{
|
||||
if (length > _ending - Offset)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
RequireBytes(length);
|
||||
|
||||
while (length-- > 0)
|
||||
{
|
||||
@@ -70,10 +79,7 @@ internal class DataReader
|
||||
|
||||
public void SkipData(long size)
|
||||
{
|
||||
if (size > _ending - Offset)
|
||||
{
|
||||
throw new IncompleteArchiveException("Unexpected end of stream.");
|
||||
}
|
||||
RequireBytes(size);
|
||||
|
||||
Offset += (int)size;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Crypto;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.SevenZip;
|
||||
|
||||
[CollectionDefinition(nameof(SevenZipHeaderAllocationTests), DisableParallelization = true)]
|
||||
public class SevenZipHeaderAllocationCollection { }
|
||||
|
||||
[Collection(nameof(SevenZipHeaderAllocationTests))]
|
||||
public class SevenZipHeaderAllocationTests
|
||||
{
|
||||
public static IEnumerable<object[]> MalformedHeaders()
|
||||
{
|
||||
var count = Number(100_000);
|
||||
yield return ["folders", Join([1, 4, 7, 11], count, [0])];
|
||||
yield return ["files", Join([1, 5], count, [0])];
|
||||
yield return ["coders", Join([1, 4, 7, 11, 1, 0], count)];
|
||||
yield return ["properties", Join([1, 4, 7, 11, 1, 0, 1, 0x21, 0x21], count)];
|
||||
yield return ["pack streams", Join([1, 4, 6, 0], count, [9, 0])];
|
||||
yield return ["input streams", Join([1, 4, 7, 11, 1, 0, 1, 0x11, 0], count, [1])];
|
||||
yield return ["output streams", Join([1, 4, 7, 11, 1, 0, 1, 0x11, 0, 1], count)];
|
||||
yield return ["zero coders", new byte[] { 1, 4, 7, 11, 1, 0, 0 }];
|
||||
yield return ["zero input streams", new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 0, 1 }];
|
||||
yield return ["zero output streams", new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 1, 0 }];
|
||||
yield return ["truncated empty bitmap", Join([1, 5], count, [14, 1, 0x80, 0])];
|
||||
// One valid Copy folder followed by forged per-folder substream counts.
|
||||
byte[] folder = [1, 4, 7, 11, 1, 0, 1, 1, 0, 12, 0, 0, 8, 13];
|
||||
yield return ["substream CRCs", Join(folder, count, [10, 1])];
|
||||
yield return ["substream sizes", Join(folder, count, [9, 0])];
|
||||
yield return ["implicit substream CRCs", Join(folder, count, [0])];
|
||||
yield return ["substream size exceeds folder", Join(folder, [2, 9, 1, 0])];
|
||||
yield return ["invalid bind index", new byte[] { 1, 4, 7, 11, 1, 0, 2, 1, 0, 1, 0, 2, 0 }];
|
||||
yield return
|
||||
[
|
||||
"invalid pack index",
|
||||
new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 2, 1, 0, 2 },
|
||||
];
|
||||
yield return
|
||||
[
|
||||
"duplicate pack index",
|
||||
new byte[] { 1, 4, 7, 11, 1, 0, 1, 0x11, 0, 2, 1, 0, 0 },
|
||||
];
|
||||
yield return
|
||||
[
|
||||
"oversized decoded metadata",
|
||||
MetadataHeader(true, (ulong)int.MaxValue + 1, 0, [1, 0]),
|
||||
];
|
||||
yield return
|
||||
[
|
||||
"overflowing inputs",
|
||||
Join(
|
||||
[1, 4, 7, 11, 1, 0, 2, 0x11, 0],
|
||||
Number(int.MaxValue),
|
||||
[1, 0x11, 0],
|
||||
Number(int.MaxValue),
|
||||
[1]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedHeaders))]
|
||||
public void SevenZipArchive_InvalidHeaderCounts_RejectBeforeAllocation(
|
||||
string name,
|
||||
byte[] header
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(header);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
#endif
|
||||
var exception = Record.Exception(() => archive.Entries.ToList());
|
||||
Assert.True(
|
||||
exception is InvalidFormatException or IncompleteArchiveException,
|
||||
$"{name}: {exception}"
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated < 512 * 1024, $"{name}: allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedHeaders))]
|
||||
public async Task SevenZipArchive_InvalidHeaderCounts_AsyncRejectBeforeAllocation(
|
||||
string name,
|
||||
byte[] header
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(header);
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetTotalAllocatedBytes(precise: true);
|
||||
#endif
|
||||
var exception = await Record.ExceptionAsync(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
Assert.True(
|
||||
exception is InvalidFormatException or IncompleteArchiveException,
|
||||
$"{name}: {exception}"
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
|
||||
Assert.True(allocated < 512 * 1024, $"{name}: allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SevenZipArchive_TruncatedDecodedMetadata_DoesNotReserveUnpackSize(
|
||||
bool encodedHeader
|
||||
)
|
||||
{
|
||||
// Copy produces only the bytes present, despite advertising 16 MiB.
|
||||
var header = MetadataHeader(encodedHeader, 16 * 1024 * 1024, 1, [1, 0]);
|
||||
using var stream = CreateArchive(header, [0]);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
#endif
|
||||
Assert.Throws<IncompleteArchiveException>(() => archive.Entries.ToList());
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_TruncatedDecodedMetadata_AsyncRejects(bool encodedHeader)
|
||||
{
|
||||
var header = MetadataHeader(encodedHeader, 16 * 1024 * 1024, 1, [1, 0]);
|
||||
using var stream = CreateArchive(header, [0]);
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetTotalAllocatedBytes(precise: true);
|
||||
#endif
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_ReportedLzma2MetadataPayload_Rejects(bool async)
|
||||
{
|
||||
// The report puts the first packed byte at the start of the next header.
|
||||
var header = MetadataHeader(false, 16 * 1024 * 1024, 1, [0x21, 0x21, 1, 0x18]);
|
||||
using var stream = CreateArchive(header);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetTotalAllocatedBytes(precise: true);
|
||||
#endif
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
#if !LEGACY_DOTNET
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
#endif
|
||||
Assert.Throws<DataErrorException>(() => archive.Entries.ToList());
|
||||
#if !LEGACY_DOTNET
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated < 512 * 1024, $"Allocated {allocated} bytes.");
|
||||
#endif
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_BitPackedEmptyFiles_AreValid(bool async)
|
||||
{
|
||||
// Nine nameless directories need only two bitmap bytes. A byte-per-file
|
||||
// bound on the remaining header would incorrectly reject this archive.
|
||||
using var stream = CreateArchive([1, 5, 9, 14, 2, 0xFF, 0x80, 0, 0]);
|
||||
if (async)
|
||||
{
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
stream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
var count = 0;
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
)
|
||||
{
|
||||
Assert.True(entry.IsDirectory);
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(9, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
Assert.Equal(9, archive.Entries.Count());
|
||||
Assert.All(archive.Entries, entry => Assert.True(entry.IsDirectory));
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_ExternalFolderCount_UsesExternalBuffer(bool async)
|
||||
{
|
||||
// Decode one byte of additional metadata, then reference it as the
|
||||
// folder-definition buffer. The inline header has ample padding.
|
||||
var header = Join(MetadataHeader(false, 1, 1, [1, 0]), [4, 7, 11, 2, 1, 0], new byte[128]);
|
||||
using var stream = CreateArchive(header, [0]);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
Assert.Throws<IncompleteArchiveException>(() => archive.Entries.ToList());
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_ExternalFolderDefinition_IsValid(bool async)
|
||||
{
|
||||
// Additional metadata contains a Copy folder. Its unpack size remains
|
||||
// inline; the file's packed byte follows the external folder definition.
|
||||
var header = Join(
|
||||
MetadataHeader(false, 3, 3, [1, 0]),
|
||||
[4, 6, 3, 1, 9, 1, 0, 7, 11, 1, 1, 0, 12, 1, 0, 8, 0, 0, 5, 1, 0, 0]
|
||||
);
|
||||
using var stream = CreateArchive(header, [1, 1, 0, 42]);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
var count = 0;
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
)
|
||||
{
|
||||
using var entryStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
|
||||
var data = new byte[1];
|
||||
Assert.Equal(1, await entryStream.ReadAsync(data, 0, 1, CancellationToken.None));
|
||||
Assert.Equal(42, data[0]);
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
using var entryStream = archive.Entries.Single().OpenEntryStream();
|
||||
Assert.Equal(42, entryStream.ReadByte());
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task SevenZipArchive_DecodedMetadataSpanningBuffers_IsValid(bool async)
|
||||
{
|
||||
// An unknown archive property forces decoding across several buffers.
|
||||
var decoded = Join([1, 2, 26], Number(20_000), new byte[20_000], [0, 0]);
|
||||
var header = MetadataHeader(true, (ulong)decoded.Length, (ulong)decoded.Length, [1, 0]);
|
||||
using var stream = CreateArchive(header, decoded);
|
||||
if (async)
|
||||
{
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
)
|
||||
{
|
||||
Assert.Fail("Expected an empty archive.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
Assert.Empty(archive.Entries);
|
||||
}
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
private static byte[] MetadataHeader(
|
||||
bool encoded,
|
||||
ulong unpackSize,
|
||||
ulong packSize,
|
||||
byte[] coder
|
||||
) =>
|
||||
Join(
|
||||
encoded ? [23] : [1, 3],
|
||||
[6, 0, 1, 9],
|
||||
Number(packSize),
|
||||
[0, 7, 11, 1, 0, 1],
|
||||
coder,
|
||||
[12],
|
||||
Number(unpackSize),
|
||||
[0, 0]
|
||||
);
|
||||
|
||||
private static byte[] Number(ulong value)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
stream.WriteEncodedUInt64(value);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] Join(params byte[][] parts) => parts.SelectMany(part => part).ToArray();
|
||||
|
||||
private static MemoryStream CreateArchive(byte[] header, byte[]? packedData = null)
|
||||
{
|
||||
packedData ??= [];
|
||||
var crc = Crc32Stream.Compute(
|
||||
Crc32Stream.DEFAULT_POLYNOMIAL,
|
||||
Crc32Stream.DEFAULT_SEED,
|
||||
header
|
||||
);
|
||||
var stream = new MemoryStream();
|
||||
SevenZipSignatureHeaderWriter.WriteFinal(
|
||||
stream,
|
||||
(ulong)packedData.Length,
|
||||
(ulong)header.Length,
|
||||
crc
|
||||
);
|
||||
stream.Write(packedData, 0, packedData.Length);
|
||||
stream.Write(header, 0, header.Length);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user