mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-25 08:24:58 +00:00
BZip2, GZip and XZ done
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Common.Options;
|
||||
|
||||
@@ -86,6 +87,17 @@ public abstract class Entry : IEntry
|
||||
|
||||
internal virtual ChecksumDescriptor Checksum => default;
|
||||
|
||||
internal virtual Stream WrapWithChecksumValidation(Stream source, ExtractionOptions options)
|
||||
{
|
||||
var checksum = Checksum;
|
||||
if (!checksum.IsAvailable)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
return new ChecksumValidationStream(source, checksum, Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry file attribute.
|
||||
/// </summary>
|
||||
|
||||
165
src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs
Normal file
165
src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Crypto;
|
||||
|
||||
namespace SharpCompress.Common.GZip;
|
||||
|
||||
internal sealed class GZipChecksumValidationStream : Stream
|
||||
{
|
||||
private readonly Stream _source;
|
||||
private readonly Stream _rawStream;
|
||||
private readonly string _entryName;
|
||||
private readonly uint? _expectedCrc;
|
||||
private readonly uint? _expectedSize;
|
||||
private readonly uint[] _crc32Table;
|
||||
private uint _seed = Crc32Stream.DEFAULT_SEED;
|
||||
private uint _size;
|
||||
private bool _validated;
|
||||
|
||||
internal GZipChecksumValidationStream(
|
||||
Stream source,
|
||||
Stream rawStream,
|
||||
string? entryName,
|
||||
uint? expectedCrc,
|
||||
uint? expectedSize
|
||||
)
|
||||
{
|
||||
_source = source;
|
||||
_rawStream = rawStream;
|
||||
_entryName = string.IsNullOrEmpty(entryName) ? "Entry" : entryName!;
|
||||
_expectedCrc = expectedCrc;
|
||||
_expectedSize = expectedSize;
|
||||
_crc32Table = Crc32Stream.InitializeTable(Crc32Stream.DEFAULT_POLYNOMIAL);
|
||||
}
|
||||
|
||||
public override bool CanRead => _source.CanRead;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => _source.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _source.Position;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => _source.Flush();
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
_source.FlushAsync(cancellationToken);
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var read = _source.Read(buffer, offset, count);
|
||||
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
var read = _source.Read(buffer);
|
||||
UpdateAndValidateAtEof(buffer[..read], read);
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
var value = _source.ReadByte();
|
||||
if (value == -1)
|
||||
{
|
||||
Validate();
|
||||
}
|
||||
else
|
||||
{
|
||||
_seed = Crc32Stream.CalculateCrc(_crc32Table, _seed, (byte)value);
|
||||
_size++;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var read = await _source
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var read = await _source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
UpdateAndValidateAtEof(buffer.Span[..read], read);
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
private void UpdateAndValidateAtEof(ReadOnlySpan<byte> buffer, int read)
|
||||
{
|
||||
if (read > 0)
|
||||
{
|
||||
_seed = Crc32Stream.CalculateCrc(_crc32Table, _seed, buffer);
|
||||
_size += unchecked((uint)read);
|
||||
return;
|
||||
}
|
||||
|
||||
Validate();
|
||||
}
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
if (_validated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_validated = true;
|
||||
|
||||
var expectedCrc = _expectedCrc;
|
||||
var expectedSize = _expectedSize;
|
||||
if (!expectedCrc.HasValue || !expectedSize.HasValue)
|
||||
{
|
||||
Span<byte> trailer = stackalloc byte[8];
|
||||
_rawStream.ReadFully(trailer);
|
||||
expectedCrc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
|
||||
expectedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer[4..]);
|
||||
}
|
||||
|
||||
var actualCrc = ~_seed;
|
||||
if (actualCrc != expectedCrc.Value)
|
||||
{
|
||||
throw new InvalidFormatException(
|
||||
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expectedCrc.Value:X8}, actual 0x{actualCrc:X8}."
|
||||
);
|
||||
}
|
||||
|
||||
if (_size != expectedSize.Value)
|
||||
{
|
||||
throw new InvalidFormatException(
|
||||
$"Size mismatch for entry '{_entryName}'. Expected {expectedSize.Value}, actual {_size}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ public partial class GZipEntry : Entry
|
||||
|
||||
public override long Crc => _filePart?.Crc ?? 0;
|
||||
|
||||
internal override Stream WrapWithChecksumValidation(Stream source, ExtractionOptions options) =>
|
||||
_filePart?.WrapWithChecksumValidation(source, Key) ?? source;
|
||||
|
||||
public override string? Key => _filePart?.FilePartName;
|
||||
|
||||
public override string? LinkTarget => null;
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Providers;
|
||||
|
||||
namespace SharpCompress.Common.GZip;
|
||||
@@ -48,7 +49,7 @@ internal sealed partial class GZipFilePart : FilePart
|
||||
)
|
||||
: base(archiveEncoding)
|
||||
{
|
||||
_stream = stream;
|
||||
_stream = SharpCompressStream.Create(stream);
|
||||
_compressionProviders = compressionProviders;
|
||||
}
|
||||
|
||||
@@ -66,6 +67,9 @@ internal sealed partial class GZipFilePart : FilePart
|
||||
return _compressionProviders.CreateDecompressStream(CompressionType.Deflate, _stream);
|
||||
}
|
||||
|
||||
internal Stream WrapWithChecksumValidation(Stream source, string? entryName) =>
|
||||
new GZipChecksumValidationStream(source, _stream, entryName, Crc, UncompressedSize);
|
||||
|
||||
internal override Stream GetRawStream() => _stream;
|
||||
|
||||
private void ReadTrailer()
|
||||
|
||||
@@ -11,13 +11,10 @@ internal static partial class IEntryExtensions
|
||||
ExtractionOptions? options
|
||||
)
|
||||
{
|
||||
if (options?.CheckCrc != false && entry is Entry typedEntry)
|
||||
options ??= new ExtractionOptions();
|
||||
if (options.CheckCrc && entry is Entry typedEntry)
|
||||
{
|
||||
var checksum = typedEntry.Checksum;
|
||||
if (checksum.IsAvailable)
|
||||
{
|
||||
return new ChecksumValidationStream(source, checksum, entry.Key);
|
||||
}
|
||||
return typedEntry.WrapWithChecksumValidation(source, options);
|
||||
}
|
||||
|
||||
return source;
|
||||
|
||||
45
tests/SharpCompress.Test/BZip2/BZip2StreamTests.cs
Normal file
45
tests/SharpCompress.Test/BZip2/BZip2StreamTests.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SharpCompress.Common;
|
||||
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));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
90
tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs
Normal file
90
tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.GZip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.GZip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipCrcExtractionTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void GZipArchive_WriteToFile_Throws_On_Crc_Mismatch()
|
||||
{
|
||||
using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
|
||||
using var archive = GZipArchive.OpenArchive(stream);
|
||||
var entry = archive.Entries.Single();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
|
||||
Assert.Throws<InvalidFormatException>(() => entry.WriteToFile(destination));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipArchive_WriteToFile_Throws_On_Size_Mismatch()
|
||||
{
|
||||
using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: false));
|
||||
using var archive = GZipArchive.OpenArchive(stream);
|
||||
var entry = archive.Entries.Single();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
|
||||
Assert.Throws<InvalidFormatException>(() => entry.WriteToFile(destination));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipReader_WriteEntryToFile_Throws_On_NonSeekable_Crc_Mismatch()
|
||||
{
|
||||
using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
|
||||
using var nonSeekableStream = new ForwardOnlyStream(stream);
|
||||
using var reader = GZipReader.OpenReader(nonSeekableStream);
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.Throws<InvalidFormatException>(() => reader.WriteEntryToFile(destination));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipArchive_WriteToFile_Skips_Trailer_Validation_When_CheckCrc_Is_False()
|
||||
{
|
||||
using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
|
||||
using var archive = GZipArchive.OpenArchive(stream);
|
||||
var entry = archive.Entries.Single();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
|
||||
entry.WriteToFile(destination, new ExtractionOptions { CheckCrc = false });
|
||||
|
||||
Assert.Equal(
|
||||
new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")).Length,
|
||||
new FileInfo(destination).Length
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GZipArchive_WriteToFileAsync_Throws_On_Crc_Mismatch()
|
||||
{
|
||||
await using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
|
||||
await using var archive = await GZipArchive.OpenAsyncArchive(stream);
|
||||
var entry = await archive.EntriesAsync.SingleAsync();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
|
||||
await entry.WriteToFileAsync(destination)
|
||||
);
|
||||
}
|
||||
|
||||
private static byte[] ReadCorruptedGZipTrailer(bool corruptCrc)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
var trailer = bytes.AsSpan(bytes.Length - 8);
|
||||
var offset = corruptCrc ? 0 : 4;
|
||||
var value = BinaryPrimitives.ReadUInt32LittleEndian(trailer[offset..]);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(trailer[offset..], value + 1);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Test.Mocks;
|
||||
@@ -54,4 +55,15 @@ public class XzStreamTests : XzTestsBase
|
||||
var uncompressed = sr.ReadToEnd();
|
||||
Assert.Equal(OriginalEmpty, uncompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throws_On_Corrupt_Block_Check()
|
||||
{
|
||||
var compressed = (byte[])Compressed.Clone();
|
||||
compressed[compressed.Length - 29] ^= 1;
|
||||
using var xz = new XZStream(new MemoryStream(compressed));
|
||||
using var output = new MemoryStream();
|
||||
|
||||
Assert.Throws<InvalidFormatException>(() => xz.CopyTo(output));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user