mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Reject stalled BZip2 decoder states after corruption
This commit is contained in:
@@ -21,26 +21,24 @@ internal partial class CBZip2InputStream
|
||||
var retChar = currentChar;
|
||||
switch (currentState)
|
||||
{
|
||||
case START_BLOCK_STATE:
|
||||
break;
|
||||
case RAND_PART_A_STATE:
|
||||
break;
|
||||
case RAND_PART_B_STATE:
|
||||
await SetupRandPartBAsync(cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case RAND_PART_C_STATE:
|
||||
await SetupRandPartCAsync(cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NO_RAND_PART_A_STATE:
|
||||
break;
|
||||
case NO_RAND_PART_B_STATE:
|
||||
await SetupNoRandPartBAsync(cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case NO_RAND_PART_C_STATE:
|
||||
await SetupNoRandPartCAsync(cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case START_BLOCK_STATE:
|
||||
case RAND_PART_A_STATE:
|
||||
case NO_RAND_PART_A_STATE:
|
||||
default:
|
||||
break;
|
||||
// Match ReadByte(): retries after a decoding error must not generate endless output.
|
||||
throw new InvalidFormatException("BZip2: invalid decoder state");
|
||||
}
|
||||
return retChar;
|
||||
}
|
||||
|
||||
@@ -263,26 +263,25 @@ internal partial class CBZip2InputStream : Stream
|
||||
var retChar = currentChar;
|
||||
switch (currentState)
|
||||
{
|
||||
case START_BLOCK_STATE:
|
||||
break;
|
||||
case RAND_PART_A_STATE:
|
||||
break;
|
||||
case RAND_PART_B_STATE:
|
||||
SetupRandPartB();
|
||||
break;
|
||||
case RAND_PART_C_STATE:
|
||||
SetupRandPartC();
|
||||
break;
|
||||
case NO_RAND_PART_A_STATE:
|
||||
break;
|
||||
case NO_RAND_PART_B_STATE:
|
||||
SetupNoRandPartB();
|
||||
break;
|
||||
case NO_RAND_PART_C_STATE:
|
||||
SetupNoRandPartC();
|
||||
break;
|
||||
case START_BLOCK_STATE:
|
||||
case RAND_PART_A_STATE:
|
||||
case NO_RAND_PART_A_STATE:
|
||||
default:
|
||||
break;
|
||||
// These transitional states can remain after a decoding error. A retry (including
|
||||
// entry disposal) must fail rather than return the same byte indefinitely.
|
||||
throw new InvalidFormatException("BZip2: invalid decoder state");
|
||||
}
|
||||
return retChar;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
@@ -11,6 +13,41 @@ namespace SharpCompress.Test.BZip2;
|
||||
|
||||
public class BZip2StreamAsyncTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BZip2Stream_Throws_On_Read_After_Corrupt_Checksum_Async()
|
||||
{
|
||||
using var compressed = new MemoryStream();
|
||||
await using (
|
||||
var writer = await BZip2Stream.CreateAsync(
|
||||
new AsyncOnlyStream(compressed),
|
||||
SharpCompress.Compressors.CompressionMode.Compress,
|
||||
false,
|
||||
leaveOpen: true,
|
||||
cancellationToken: CancellationToken.None
|
||||
)
|
||||
)
|
||||
{
|
||||
var data = Encoding.ASCII.GetBytes("BZip2 checksum validation test data.");
|
||||
await writer.WriteAsync(data, 0, data.Length, CancellationToken.None);
|
||||
}
|
||||
|
||||
var bytes = compressed.ToArray();
|
||||
bytes[^5] ^= 1;
|
||||
await using var stream = await BZip2Stream.CreateAsync(
|
||||
new AsyncOnlyStream(new MemoryStream(bytes)),
|
||||
SharpCompress.Compressors.CompressionMode.Decompress,
|
||||
false,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
|
||||
await Assert.ThrowsAsync<ArchiveOperationException>(() =>
|
||||
stream.CopyToAsync(Stream.Null, 81920, CancellationToken.None)
|
||||
);
|
||||
await Assert.ThrowsAsync<InvalidFormatException>(() =>
|
||||
stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)
|
||||
);
|
||||
}
|
||||
|
||||
private byte[] CreateTestData(int size)
|
||||
{
|
||||
var data = new byte[size];
|
||||
|
||||
@@ -24,6 +24,8 @@ public class BZip2StreamTests
|
||||
using var output = new MemoryStream();
|
||||
|
||||
Assert.Throws<ArchiveOperationException>(() => stream.CopyTo(output));
|
||||
Assert.Throws<InvalidFormatException>(() => stream.ReadByte());
|
||||
Assert.Throws<InvalidFormatException>(() => stream.Read(new byte[1], 0, 1));
|
||||
}
|
||||
|
||||
// A stream that ends exactly where a block header is expected (no stream footer) is the shape a caller
|
||||
|
||||
167
tests/SharpCompress.Test/Zip/ZipBZip2CorruptionTests.cs
Normal file
167
tests/SharpCompress.Test/Zip/ZipBZip2CorruptionTests.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipBZip2CorruptionTests : TestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(240, 0x8d, 0x5a, false)]
|
||||
[InlineData(11318, 0x35, 0xa8, false)]
|
||||
[InlineData(41884, 0x12, 0x89, false)]
|
||||
[InlineData(240, 0x8d, 0x5a, true)]
|
||||
[InlineData(11318, 0x35, 0xa8, true)]
|
||||
[InlineData(41884, 0x12, 0x89, true)]
|
||||
public void Zip_BZip2_Reader_Throws_On_Corruption(
|
||||
int offset,
|
||||
byte original,
|
||||
byte replacement,
|
||||
bool skipEntry
|
||||
)
|
||||
{
|
||||
using var stream = new ForwardOnlyStream(
|
||||
CreateCorruptArchive(offset, original, replacement)
|
||||
);
|
||||
using var reader = ReaderFactory.OpenReader(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
var exception = Assert.ThrowsAny<SharpCompressException>(() =>
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
// Disposal must also terminate when a failed read leaves the decoder between states.
|
||||
using var entry = reader.OpenEntryStream();
|
||||
if (!skipEntry)
|
||||
{
|
||||
entry.CopyTo(Stream.Null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Assert.StartsWith("BZip2", exception.Message);
|
||||
if (!skipEntry)
|
||||
{
|
||||
Assert.IsType<InvalidFormatException>(exception);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(240, 0x8d, 0x5a, false)]
|
||||
[InlineData(11318, 0x35, 0xa8, false)]
|
||||
[InlineData(41884, 0x12, 0x89, false)]
|
||||
[InlineData(240, 0x8d, 0x5a, true)]
|
||||
[InlineData(11318, 0x35, 0xa8, true)]
|
||||
[InlineData(41884, 0x12, 0x89, true)]
|
||||
public async Task Zip_BZip2_Reader_Throws_On_Corruption_Async(
|
||||
int offset,
|
||||
byte original,
|
||||
byte replacement,
|
||||
bool skipEntry
|
||||
)
|
||||
{
|
||||
using var stream = new AsyncOnlyStream(
|
||||
new ForwardOnlyStream(CreateCorruptArchive(offset, original, replacement))
|
||||
);
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(
|
||||
stream,
|
||||
ReaderOptions.ForExternalStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
|
||||
var exception = await Assert.ThrowsAnyAsync<SharpCompressException>(async () =>
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync(CancellationToken.None))
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entry = await reader.OpenEntryStreamAsync(CancellationToken.None);
|
||||
#else
|
||||
await using var entry = await reader.OpenEntryStreamAsync(CancellationToken.None);
|
||||
#endif
|
||||
if (!skipEntry)
|
||||
{
|
||||
await entry.CopyToAsync(Stream.Null, 81920, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Assert.StartsWith("BZip2", exception.Message);
|
||||
if (!skipEntry)
|
||||
{
|
||||
Assert.IsType<InvalidFormatException>(exception);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(240, 0x8d, 0x5a)]
|
||||
[InlineData(11318, 0x35, 0xa8)]
|
||||
[InlineData(41884, 0x12, 0x89)]
|
||||
public void Zip_BZip2_Archive_Throws_On_Corruption(int offset, byte original, byte replacement)
|
||||
{
|
||||
using var stream = CreateCorruptArchive(offset, original, replacement);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream, ReaderOptions.ForExternalStream);
|
||||
|
||||
Assert.ThrowsAny<SharpCompressException>(() =>
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
entryStream.CopyTo(Stream.Null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(240, 0x8d, 0x5a)]
|
||||
[InlineData(11318, 0x35, 0xa8)]
|
||||
[InlineData(41884, 0x12, 0x89)]
|
||||
public async Task Zip_BZip2_Archive_Throws_On_Corruption_Async(
|
||||
int offset,
|
||||
byte original,
|
||||
byte replacement
|
||||
)
|
||||
{
|
||||
using var stream = new AsyncOnlyStream(CreateCorruptArchive(offset, original, replacement));
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
stream,
|
||||
ReaderOptions.ForExternalStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
|
||||
await Assert.ThrowsAnyAsync<SharpCompressException>(async () =>
|
||||
{
|
||||
await foreach (var entry in archive.EntriesAsync)
|
||||
{
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await entry.OpenEntryStreamAsync(
|
||||
CancellationToken.None
|
||||
);
|
||||
#else
|
||||
await using var entryStream = await entry.OpenEntryStreamAsync(
|
||||
CancellationToken.None
|
||||
);
|
||||
#endif
|
||||
await entryStream.CopyToAsync(Stream.Null, 81920, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static MemoryStream CreateCorruptArchive(int offset, byte original, byte replacement)
|
||||
{
|
||||
// Mutate the existing fixture in memory so each regression differs by exactly one byte.
|
||||
var bytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.dd.zip"));
|
||||
Assert.Equal(original, bytes[offset]);
|
||||
bytes[offset] = replacement;
|
||||
return new MemoryStream(bytes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user