Validate 7z header counts before allocation

Bound nested metadata counts and coder properties by their backing data, validate file and stream consistency, and buffer decoded headers incrementally instead of reserving attacker-declared output sizes.

Add synchronous and asynchronous allocation regressions for GHSA-vwpg-q8vc-g6fg, including external metadata and valid bit-packed empty entries.
This commit is contained in:
Adam Hathcock
2026-11-09 11:53:34 +00:00
parent 839dcb1ffb
commit 8ec2188ee0
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;
}
}