Files
sharpcompress/tests/SharpCompress.Test/BZip2/BZip2StreamTests.cs

140 lines
4.8 KiB
C#
Raw Permalink Normal View History

BZip2: add opt-in tolerateTruncatedStream to decode a footerless/partial stream Add a `tolerateTruncatedStream` option (default false) to `BZip2Stream.Create`/`CreateAsync`, threaded through to `CBZip2InputStream`. When enabled, the decoder accepts a stream that has no trailing footer - e.g. a truncated stream, or a sub-range of blocks extracted for random access: - An end-of-input reached while reading a block/footer header (a true block boundary, tracked by `expectingBlockStart`) is treated as a normal end of stream instead of throwing `ArchiveOperationException("BZip2 compressed file ends unexpectedly")`. EOF in the middle of a block, the block CRC, or the Huffman tables still throws. - The whole-stream combined CRC in the footer is not verified, since a partial decode's running combined CRC won't match the stored whole-stream value. Per-block CRCs are still enforced. Default behaviour is unchanged (the flag defaults to false) and the change is applied symmetrically to the sync and async read paths. Tests (BZip2StreamTests): - a footerless header-only stream decodes to empty with the flag and throws without it; - a real block followed by end-of-input at the next block boundary decodes with the flag and throws without it; - a corrupted whole-stream combined CRC is tolerated with the flag and fatal without it; - a complete, well-formed stream still round-trips with the flag set. All existing BZip2 tests continue to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:36:15 +10:00
using System;
2026-06-15 09:59:48 +01:00
using System.IO;
using System.Text;
using SharpCompress.Common;
BZip2: add opt-in tolerateTruncatedStream to decode a footerless/partial stream Add a `tolerateTruncatedStream` option (default false) to `BZip2Stream.Create`/`CreateAsync`, threaded through to `CBZip2InputStream`. When enabled, the decoder accepts a stream that has no trailing footer - e.g. a truncated stream, or a sub-range of blocks extracted for random access: - An end-of-input reached while reading a block/footer header (a true block boundary, tracked by `expectingBlockStart`) is treated as a normal end of stream instead of throwing `ArchiveOperationException("BZip2 compressed file ends unexpectedly")`. EOF in the middle of a block, the block CRC, or the Huffman tables still throws. - The whole-stream combined CRC in the footer is not verified, since a partial decode's running combined CRC won't match the stored whole-stream value. Per-block CRCs are still enforced. Default behaviour is unchanged (the flag defaults to false) and the change is applied symmetrically to the sync and async read paths. Tests (BZip2StreamTests): - a footerless header-only stream decodes to empty with the flag and throws without it; - a real block followed by end-of-input at the next block boundary decodes with the flag and throws without it; - a corrupted whole-stream combined CRC is tolerated with the flag and fatal without it; - a complete, well-formed stream still round-trips with the flag set. All existing BZip2 tests continue to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:36:15 +10:00
using SharpCompress.Compressors;
2026-06-15 09:59:48 +01:00
using SharpCompress.Compressors.BZip2;
using Xunit;
namespace SharpCompress.Test.BZip2;
public class BZip2StreamTests
{
[Fact]
public void BZip2Stream_Throws_On_Corrupt_Checksum()
{
var compressed = Compress("BZip2 checksum validation test data.");
compressed[^5] ^= 1;
using var stream = BZip2Stream.Create(
new MemoryStream(compressed),
SharpCompress.Compressors.CompressionMode.Decompress,
false
);
using var output = new MemoryStream();
Assert.Throws<ArchiveOperationException>(() => stream.CopyTo(output));
}
BZip2: add opt-in tolerateTruncatedStream to decode a footerless/partial stream Add a `tolerateTruncatedStream` option (default false) to `BZip2Stream.Create`/`CreateAsync`, threaded through to `CBZip2InputStream`. When enabled, the decoder accepts a stream that has no trailing footer - e.g. a truncated stream, or a sub-range of blocks extracted for random access: - An end-of-input reached while reading a block/footer header (a true block boundary, tracked by `expectingBlockStart`) is treated as a normal end of stream instead of throwing `ArchiveOperationException("BZip2 compressed file ends unexpectedly")`. EOF in the middle of a block, the block CRC, or the Huffman tables still throws. - The whole-stream combined CRC in the footer is not verified, since a partial decode's running combined CRC won't match the stored whole-stream value. Per-block CRCs are still enforced. Default behaviour is unchanged (the flag defaults to false) and the change is applied symmetrically to the sync and async read paths. Tests (BZip2StreamTests): - a footerless header-only stream decodes to empty with the flag and throws without it; - a real block followed by end-of-input at the next block boundary decodes with the flag and throws without it; - a corrupted whole-stream combined CRC is tolerated with the flag and fatal without it; - a complete, well-formed stream still round-trips with the flag set. All existing BZip2 tests continue to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:36:15 +10:00
// A stream that ends exactly where a block header is expected (no stream footer) is the shape a caller
// sees when decoding a truncated stream or a sub-range of blocks extracted for random access. "BZh9" is
// a valid header with no blocks and no footer, so the very first block-header read hits end-of-input.
[Fact]
public void BZip2Stream_TolerateTruncatedStream_DecodesFooterlessStreamAsEmpty()
{
var headerOnly = Encoding.ASCII.GetBytes("BZh9");
Assert.Throws<ArchiveOperationException>(() =>
Decompress(headerOnly, tolerateTruncatedStream: false)
);
Assert.Empty(Decompress(headerOnly, tolerateTruncatedStream: true));
}
// A real block followed by end-of-input at the next block boundary: decode a complete stream, then
// append another header that stops before its first block. With tolerance the first stream's data comes
// back and the truncated continuation ends cleanly; without it, the end-of-input throws.
[Fact]
public void BZip2Stream_TolerateTruncatedStream_DecodesStreamTruncatedAtBlockBoundary()
{
const string text = "Some data that bzip2 will put into a single block.";
var truncated = Concat(Compress(text), Encoding.ASCII.GetBytes("BZh9"));
Assert.Throws<ArchiveOperationException>(() =>
Decompress(truncated, tolerateTruncatedStream: false, decompressConcatenated: true)
);
var result = Decompress(
truncated,
tolerateTruncatedStream: true,
decompressConcatenated: true
);
Assert.Equal(text, Encoding.ASCII.GetString(result));
}
// A partial decode's running combined CRC won't match the whole-stream value in the footer, so the
// flag skips that whole-stream check (per-block CRCs are still enforced). Corrupting the stored
// combined CRC is fatal by default but tolerated with the flag.
[Fact]
public void BZip2Stream_TolerateTruncatedStream_SkipsWholeStreamCrc()
{
const string text = "BZip2 combined-CRC validation test data.";
var compressed = Compress(text);
compressed[^5] ^= 1;
Assert.Throws<ArchiveOperationException>(() =>
Decompress(compressed, tolerateTruncatedStream: false)
);
var result = Decompress(compressed, tolerateTruncatedStream: true);
Assert.Equal(text, Encoding.ASCII.GetString(result));
}
// The flag must not change decoding of a normal, well-formed stream.
[Fact]
public void BZip2Stream_TolerateTruncatedStream_StillDecodesCompleteStream()
{
const string text =
"Round trip with tolerateTruncatedStream set on a complete, valid stream.";
var result = Decompress(Compress(text), tolerateTruncatedStream: true);
Assert.Equal(text, Encoding.ASCII.GetString(result));
}
private static byte[] Decompress(
byte[] compressed,
bool tolerateTruncatedStream,
bool decompressConcatenated = false
)
{
using var stream = BZip2Stream.Create(
new MemoryStream(compressed),
CompressionMode.Decompress,
decompressConcatenated,
leaveOpen: false,
tolerateTruncatedStream: tolerateTruncatedStream
);
using var output = new MemoryStream();
stream.CopyTo(output);
return output.ToArray();
}
private static byte[] Concat(byte[] a, byte[] b)
{
var result = new byte[a.Length + b.Length];
Buffer.BlockCopy(a, 0, result, 0, a.Length);
Buffer.BlockCopy(b, 0, result, a.Length, b.Length);
return result;
}
2026-06-15 09:59:48 +01:00
private static byte[] Compress(string value)
{
using var memoryStream = new MemoryStream();
using (
var bzip2Stream = BZip2Stream.Create(
memoryStream,
SharpCompress.Compressors.CompressionMode.Compress,
false,
leaveOpen: true
)
)
{
var bytes = Encoding.ASCII.GetBytes(value);
bzip2Stream.Write(bytes, 0, bytes.Length);
}
return memoryStream.ToArray();
}
}