Merge commit from fork

Prevent excessive allocations from forged 7z header fields
This commit is contained in:
Adam Hathcock
2026-11-12 15:07:57 +00:00
committed by GitHub
6 changed files with 615 additions and 35 deletions

View File

@@ -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.

View File

@@ -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++)
{

View File

@@ -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++)
{

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}