From 2f040283fc53c7eec9e70118ed670302de4a7cb2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 13 Nov 2026 09:36:55 +0000 Subject: [PATCH] Preserve legacy 7z property size compatibility during preflight --- .../Common/SevenZip/ArchiveReader.Async.cs | 6 +- .../Common/SevenZip/ArchiveReader.cs | 18 +++- .../SevenZip/SevenZipHeaderAllocationTests.cs | 98 +++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs b/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs index d22faa16..ae1d141a 100644 --- a/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs +++ b/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs @@ -338,7 +338,11 @@ internal sealed partial class ArchiveReader } var numFiles = ReadNum(); - ValidateFileCount(numFiles, unpackSizes.Count); + ValidateFileCount( + numFiles, + unpackSizes.Count, + db._majorVersion > 0 || db._minorVersion > 2 + ); db._files = new List(numFiles); for (var i = 0; i < numFiles; i++) { diff --git a/src/SharpCompress/Common/SevenZip/ArchiveReader.cs b/src/SharpCompress/Common/SevenZip/ArchiveReader.cs index 8d5ae0e0..d2bb2e69 100644 --- a/src/SharpCompress/Common/SevenZip/ArchiveReader.cs +++ b/src/SharpCompress/Common/SevenZip/ArchiveReader.cs @@ -714,8 +714,18 @@ internal partial class ArchiveReader return (int)size; } - private void ValidateFileCount(int numFiles, int numStreams) + private void ValidateFileCount(int numFiles, int numStreams, bool checkRecordsSize) { + if (!checkRecordsSize) + { + // Before 0.3, known properties may have inaccurate sizes, so walking + // them by their declared lengths would disagree with the real parser. + // Bound the allocation by the minimum possible empty-stream bitmap; + // ReadBitVector separately bounds the actual bitmap at its read position. + var minimumEmptyStreams = Math.Max(0L, (long)numFiles - numStreams); + _currentReader.RequireBytes((minimumEmptyStreams + 7) / 8); + return; + } // 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. @@ -914,7 +924,11 @@ internal partial class ArchiveReader } var numFiles = ReadNum(); - ValidateFileCount(numFiles, unpackSizes.Count); + ValidateFileCount( + numFiles, + unpackSizes.Count, + db._majorVersion > 0 || db._minorVersion > 2 + ); db._files = new List(numFiles); for (var i = 0; i < numFiles; i++) { diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipHeaderAllocationTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipHeaderAllocationTests.cs index 18db66b5..90f4dc24 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipHeaderAllocationTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipHeaderAllocationTests.cs @@ -356,6 +356,104 @@ public class SevenZipHeaderAllocationTests Assert.True(stream.CanRead); } + [Theory] + [InlineData(2, 0, false)] + [InlineData(2, 0, true)] + [InlineData(2, 127, false)] + [InlineData(2, 127, true)] + [InlineData(3, 0, false)] + [InlineData(3, 0, true)] + public async Task SevenZipArchive_Property_Size_Validation_Is_Version_Aware( + byte minorVersion, + byte declaredSize, + bool useAsync + ) + { + // Both known properties have deliberately inaccurate sizes, including + // a name preceding the empty-stream bitmap. + using var stream = CreateArchive([ + 1, + 5, + 1, + 17, + declaredSize, + 0, + (byte)'x', + 0, + 0, + 0, + 14, + declaredSize, + 0x80, + 0, + 0, + ]); + stream.GetBuffer()[7] = minorVersion; + if (useAsync) + { + using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false); + await using var archive = await ArchiveFactory.OpenAsyncArchive( + asyncStream, + cancellationToken: CancellationToken.None + ); + if (minorVersion > 2) + { + await Assert.ThrowsAnyAsync(async () => + await archive.EntriesAsync.ToListAsync(CancellationToken.None) + ); + } + else + { + var entry = Assert.Single( + await archive.EntriesAsync.ToListAsync(CancellationToken.None) + ); + Assert.True(entry.IsDirectory); + Assert.Equal("x", entry.Key); + } + } + else + { + using var archive = ArchiveFactory.OpenArchive(stream); + if (minorVersion > 2) + { + Assert.ThrowsAny(() => archive.Entries.ToList()); + } + else + { + var entry = Assert.Single(archive.Entries); + Assert.True(entry.IsDirectory); + Assert.Equal("x", entry.Key); + } + } + Assert.True(stream.CanRead); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task SevenZipArchive_Legacy_File_Count_Is_Bounded_By_Available_Metadata( + bool useAsync + ) + { + using var stream = CreateArchive(Join([1, 5], Number(100_000), [14, 0, 0x80, 0])); + stream.GetBuffer()[7] = 2; + if (useAsync) + { + await using var archive = await ArchiveFactory.OpenAsyncArchive( + stream, + cancellationToken: CancellationToken.None + ); + await Assert.ThrowsAsync(async () => + await archive.EntriesAsync.ToListAsync(CancellationToken.None) + ); + } + else + { + using var archive = ArchiveFactory.OpenArchive(stream); + Assert.Throws(() => archive.Entries.ToList()); + } + } + private static byte[] MetadataHeader( bool encoded, ulong unpackSize,