diff --git a/src/SharpCompress/Compressors/Xz/Crc32.cs b/src/SharpCompress/Compressors/Xz/Crc32.cs index be303898..acea8ecd 100644 --- a/src/SharpCompress/Compressors/Xz/Crc32.cs +++ b/src/SharpCompress/Compressors/Xz/Crc32.cs @@ -52,6 +52,9 @@ public static class Crc32 return createTable; } + public static uint Update(uint seed, ReadOnlySpan buffer) => + CalculateHash(InitializeTable(DefaultPolynomial), seed, buffer); + private static uint CalculateHash(uint[] table, uint seed, ReadOnlySpan buffer) { var crc = seed; diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs b/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs index 7c6ab8ed..cc3c31fe 100644 --- a/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs +++ b/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs @@ -34,6 +34,7 @@ public sealed partial class XZBlock bytesRead = await _decomStream .ReadAsync(buffer, offset, count, cancellationToken) .ConfigureAwait(false); + UpdateCheck(buffer, offset, bytesRead); } if (bytesRead != count) @@ -74,9 +75,10 @@ public sealed partial class XZBlock private async ValueTask CheckCrcAsync(CancellationToken cancellationToken = default) { var crc = new byte[_checkSize]; - await BaseStream.ReadAsync(crc, 0, _checkSize, cancellationToken).ConfigureAwait(false); - // Actually do a check (and read in the bytes - // into the function throughout the stream read). + await BaseStream + .ReadExactAsync(crc, 0, _checkSize, cancellationToken) + .ConfigureAwait(false); + VerifyCheck(crc); _crcChecked = true; } diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.cs b/src/SharpCompress/Compressors/Xz/XZBlock.cs index 00c967f7..778f33b5 100644 --- a/src/SharpCompress/Compressors/Xz/XZBlock.cs +++ b/src/SharpCompress/Compressors/Xz/XZBlock.cs @@ -1,9 +1,11 @@ #nullable disable using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; @@ -19,7 +21,11 @@ public sealed partial class XZBlock : XZReadOnlyStream public ulong? UncompressedSize { get; private set; } public Stack Filters { get; private set; } = new(); public bool HeaderIsLoaded { get; private set; } + private readonly CheckType _checkType; private readonly int _checkSize; + private uint _crc32 = Crc32.DefaultSeed; + private ulong _crc64 = Crc64.DefaultSeed; + private readonly SHA256 _sha256; private bool _streamConnected; private int _numFilters; private byte _blockHeaderSizeByte; @@ -32,7 +38,12 @@ public sealed partial class XZBlock : XZReadOnlyStream public XZBlock(Stream stream, CheckType checkType, int checkSize) : base(stream) { + _checkType = checkType; _checkSize = checkSize; + if (checkType == CheckType.SHA256) + { + _sha256 = SHA256.Create(); + } _startPosition = stream.Position; } @@ -52,6 +63,7 @@ public sealed partial class XZBlock : XZReadOnlyStream if (!_endOfStream) { bytesRead = _decomStream.Read(buffer, offset, count); + UpdateCheck(buffer, offset, bytesRead); } if (bytesRead != count) @@ -90,12 +102,71 @@ public sealed partial class XZBlock : XZReadOnlyStream private void CheckCrc() { var crc = new byte[_checkSize]; - BaseStream.Read(crc, 0, _checkSize); - // Actually do a check (and read in the bytes - // into the function throughout the stream read). + BaseStream.ReadExact(crc, 0, _checkSize); + VerifyCheck(crc); _crcChecked = true; } + private void UpdateCheck(byte[] buffer, int offset, int count) + { + if (count == 0 || _checkType == CheckType.NONE) + { + return; + } + + var bytes = buffer.AsSpan(offset, count); + switch (_checkType) + { + case CheckType.CRC32: + _crc32 = Crc32.Update(_crc32, bytes); + break; + case CheckType.CRC64: + Crc64.Table ??= Crc64.CreateTable(Crc64.Iso3309Polynomial); + _crc64 = Crc64.CalculateHash(_crc64, Crc64.Table, bytes); + break; + case CheckType.SHA256: + _sha256.TransformBlock(buffer, offset, count, null, 0); + break; + } + } + + private void VerifyCheck(byte[] expected) + { + var actual = _checkType switch + { + CheckType.NONE => Array.Empty(), + CheckType.CRC32 => GetLittleEndianBytes(~_crc32), + CheckType.CRC64 => GetLittleEndianBytes(_crc64), + CheckType.SHA256 => FinalizeSha256Check(), + _ => throw new InvalidFormatException("Unsupported XZ check type"), + }; + + if (!expected.SequenceEqual(actual)) + { + throw new InvalidFormatException("Block check corrupt"); + } + } + + private static byte[] GetLittleEndianBytes(uint value) + { + var bytes = new byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + return bytes; + } + + private static byte[] GetLittleEndianBytes(ulong value) + { + var bytes = new byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); + return bytes; + } + + private byte[] FinalizeSha256Check() + { + _sha256.TransformFinalBlock(Array.Empty(), 0, 0); + return _sha256.Hash; + } + private void ConnectStream() { _decomStream = BaseStream; diff --git a/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs index 9f576d9a..5dd0348f 100644 --- a/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs @@ -2,14 +2,27 @@ using System; using System.Buffers; using System.IO; using System.Threading.Tasks; +using SharpCompress.Common; using SharpCompress.Compressors.LZMA; +using SharpCompress.Compressors.Xz; using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test.Streams; -public class LzmaStreamAsyncTests +public class LzmaStreamAsyncTests : TestBase { + [Fact] + public async ValueTask TestLzma2Decompress() + { + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "bad-1-lzma2-7.xz")); + + var xz = new XZStream(stream); + await Assert.ThrowsAsync(async () => + await xz.TransferToAsync(Stream.Null, long.MaxValue) + ); + } + [Fact] public async ValueTask TestLzma2Decompress1ByteAsync() { diff --git a/tests/TestArchives/Archives/bad-1-lzma2-7.xz b/tests/TestArchives/Archives/bad-1-lzma2-7.xz new file mode 100644 index 00000000..8cc711c1 Binary files /dev/null and b/tests/TestArchives/Archives/bad-1-lzma2-7.xz differ