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;
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|