diff --git a/src/SharpCompress/Compressors/Xz/BinaryUtils.cs b/src/SharpCompress/Compressors/Xz/BinaryUtils.cs index 8e08ff985..12cb29586 100644 --- a/src/SharpCompress/Compressors/Xz/BinaryUtils.cs +++ b/src/SharpCompress/Compressors/Xz/BinaryUtils.cs @@ -1,6 +1,8 @@ using System; using System.Buffers.Binary; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.Xz; @@ -30,6 +32,28 @@ public static int ReadLittleEndianInt32(this Stream stream) internal static uint ReadLittleEndianUInt32(this Stream stream) => unchecked((uint)ReadLittleEndianInt32(stream)); + public static async Task ReadLittleEndianInt32Async( + this Stream stream, + CancellationToken cancellationToken = default + ) + { + var bytes = new byte[4]; + var read = await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false); + if (!read) + { + throw new EndOfStreamException(); + } + return BinaryPrimitives.ReadInt32LittleEndian(bytes); + } + + internal static async Task ReadLittleEndianUInt32Async( + this Stream stream, + CancellationToken cancellationToken = default + ) => + unchecked( + (uint)await ReadLittleEndianInt32Async(stream, cancellationToken).ConfigureAwait(false) + ); + internal static byte[] ToBigEndianBytes(this uint uint32) { var result = BitConverter.GetBytes(uint32); diff --git a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs index 8a0d81a3e..f5613d661 100644 --- a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs +++ b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Compressors.Xz; @@ -39,4 +41,75 @@ public static ulong ReadXZInteger(this BinaryReader reader, int MaxBytes = 9) } return Output; } + + public static async Task ReadXZIntegerAsync( + this BinaryReader reader, + CancellationToken cancellationToken = default, + int MaxBytes = 9 + ) + { + if (MaxBytes <= 0) + { + throw new ArgumentOutOfRangeException(nameof(MaxBytes)); + } + + if (MaxBytes > 9) + { + MaxBytes = 9; + } + + var LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false); + var Output = (ulong)LastByte & 0x7F; + + var i = 0; + while ((LastByte & 0x80) != 0) + { + if (++i >= MaxBytes) + { + throw new InvalidFormatException(); + } + + LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false); + if (LastByte == 0) + { + throw new InvalidFormatException(); + } + + Output |= ((ulong)(LastByte & 0x7F)) << (i * 7); + } + return Output; + } + + public static async Task ReadByteAsync( + this BinaryReader reader, + CancellationToken cancellationToken = default + ) + { + var buffer = new byte[1]; + var bytesRead = await reader + .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead != 1) + { + throw new EndOfStreamException(); + } + return buffer[0]; + } + + public static async Task ReadBytesAsync( + this BinaryReader reader, + int count, + CancellationToken cancellationToken = default + ) + { + var buffer = new byte[count]; + var bytesRead = await reader + .BaseStream.ReadAsync(buffer, 0, count, cancellationToken) + .ConfigureAwait(false); + if (bytesRead != count) + { + throw new EndOfStreamException(); + } + return buffer; + } } diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.cs b/src/SharpCompress/Compressors/Xz/XZBlock.cs index cdb075ef8..45e11745a 100644 --- a/src/SharpCompress/Compressors/Xz/XZBlock.cs +++ b/src/SharpCompress/Compressors/Xz/XZBlock.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Compressors.Xz.Filters; @@ -72,6 +74,49 @@ public override int Read(byte[] buffer, int offset, int count) return bytesRead; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var bytesRead = 0; + if (!HeaderIsLoaded) + { + await LoadHeaderAsync(cancellationToken).ConfigureAwait(false); + } + + if (!_streamConnected) + { + ConnectStream(); + } + + if (!_endOfStream) + { + bytesRead = await _decomStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + + if (bytesRead != count) + { + _endOfStream = true; + } + + if (_endOfStream && !_paddingSkipped) + { + await SkipPaddingAsync(cancellationToken).ConfigureAwait(false); + } + + if (_endOfStream && !_crcChecked) + { + await CheckCrcAsync(cancellationToken).ConfigureAwait(false); + } + + return bytesRead; + } + private void SkipPadding() { var bytes = (BaseStream.Position - _startPosition) % 4; @@ -87,6 +132,23 @@ private void SkipPadding() _paddingSkipped = true; } + private async Task SkipPaddingAsync(CancellationToken cancellationToken = default) + { + var bytes = (BaseStream.Position - _startPosition) % 4; + if (bytes > 0) + { + var paddingBytes = new byte[4 - bytes]; + await BaseStream + .ReadAsync(paddingBytes, 0, paddingBytes.Length, cancellationToken) + .ConfigureAwait(false); + if (paddingBytes.Any(b => b != 0)) + { + throw new InvalidFormatException("Padding bytes were non-null"); + } + } + _paddingSkipped = true; + } + private void CheckCrc() { var crc = new byte[_checkSize]; @@ -96,6 +158,15 @@ private void CheckCrc() _crcChecked = true; } + private async Task 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). + _crcChecked = true; + } + private void ConnectStream() { _decomStream = BaseStream; @@ -123,6 +194,21 @@ private void LoadHeader() HeaderIsLoaded = true; } + private async Task LoadHeaderAsync(CancellationToken cancellationToken = default) + { + await ReadHeaderSizeAsync(cancellationToken).ConfigureAwait(false); + var headerCache = await CacheHeaderAsync(cancellationToken).ConfigureAwait(false); + + using (var cache = new MemoryStream(headerCache)) + using (var cachedReader = new BinaryReader(cache)) + { + cachedReader.BaseStream.Position = 1; // skip the header size byte + ReadBlockFlags(cachedReader); + ReadFilters(cachedReader); + } + HeaderIsLoaded = true; + } + private void ReadHeaderSize() { _blockHeaderSizeByte = (byte)BaseStream.ReadByte(); @@ -132,6 +218,17 @@ private void ReadHeaderSize() } } + private async Task ReadHeaderSizeAsync(CancellationToken cancellationToken = default) + { + var buffer = new byte[1]; + await BaseStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); + _blockHeaderSizeByte = buffer[0]; + if (_blockHeaderSizeByte == 0) + { + throw new XZIndexMarkerReachedException(); + } + } + private byte[] CacheHeader() { var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4]; @@ -139,7 +236,7 @@ private byte[] CacheHeader() var read = BaseStream.Read(blockHeaderWithoutCrc, 1, BlockHeaderSize - 5); if (read != BlockHeaderSize - 5) { - throw new EndOfStreamException("Reached end of stream unexectedly"); + throw new EndOfStreamException("Reached end of stream unexpectedly"); } var crc = BaseStream.ReadLittleEndianUInt32(); @@ -152,6 +249,30 @@ private byte[] CacheHeader() return blockHeaderWithoutCrc; } + private async Task CacheHeaderAsync(CancellationToken cancellationToken = default) + { + var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4]; + blockHeaderWithoutCrc[0] = _blockHeaderSizeByte; + var read = await BaseStream + .ReadAsync(blockHeaderWithoutCrc, 1, BlockHeaderSize - 5, cancellationToken) + .ConfigureAwait(false); + if (read != BlockHeaderSize - 5) + { + throw new EndOfStreamException("Reached end of stream unexpectedly"); + } + + var crc = await BaseStream + .ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + var calcCrc = Crc32.Compute(blockHeaderWithoutCrc); + if (crc != calcCrc) + { + throw new InvalidFormatException("Block header corrupt"); + } + + return blockHeaderWithoutCrc; + } + private void ReadBlockFlags(BinaryReader reader) { var blockFlags = reader.ReadByte(); diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.cs b/src/SharpCompress/Compressors/Xz/XZFooter.cs index 293b1d663..09c95b1a0 100644 --- a/src/SharpCompress/Compressors/Xz/XZFooter.cs +++ b/src/SharpCompress/Compressors/Xz/XZFooter.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -27,6 +29,16 @@ public static XZFooter FromStream(Stream stream) return footer; } + public static async Task FromStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var footer = new XZFooter(new BinaryReader(stream, Encoding.UTF8, true)); + await footer.ProcessAsync(cancellationToken).ConfigureAwait(false); + return footer; + } + public void Process() { var crc = _reader.ReadLittleEndianUInt32(); @@ -49,4 +61,29 @@ public void Process() throw new InvalidFormatException("Magic footer missing"); } } + + public async Task ProcessAsync(CancellationToken cancellationToken = default) + { + var crc = await _reader + .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + var footerBytes = await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false); + var myCrc = Crc32.Compute(footerBytes); + if (crc != myCrc) + { + throw new InvalidFormatException("Footer corrupt"); + } + + using (var stream = new MemoryStream(footerBytes)) + using (var reader = new BinaryReader(stream)) + { + BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4; + StreamFlags = reader.ReadBytes(2); + } + var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); + if (!magBy.AsSpan().SequenceEqual(_magicBytes)) + { + throw new InvalidFormatException("Magic footer missing"); + } + } } diff --git a/src/SharpCompress/Compressors/Xz/XZHeader.cs b/src/SharpCompress/Compressors/Xz/XZHeader.cs index d6001f12d..1aee619bb 100644 --- a/src/SharpCompress/Compressors/Xz/XZHeader.cs +++ b/src/SharpCompress/Compressors/Xz/XZHeader.cs @@ -1,6 +1,8 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -23,12 +25,28 @@ public static XZHeader FromStream(Stream stream) return header; } + public static async Task FromStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var header = new XZHeader(new BinaryReader(stream, Encoding.UTF8, true)); + await header.ProcessAsync(cancellationToken).ConfigureAwait(false); + return header; + } + public void Process() { CheckMagicBytes(_reader.ReadBytes(6)); ProcessStreamFlags(); } + public async Task ProcessAsync(CancellationToken cancellationToken = default) + { + CheckMagicBytes(await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false)); + await ProcessStreamFlagsAsync(cancellationToken).ConfigureAwait(false); + } + private void ProcessStreamFlags() { var streamFlags = _reader.ReadBytes(2); @@ -47,6 +65,26 @@ private void ProcessStreamFlags() } } + private async Task ProcessStreamFlagsAsync(CancellationToken cancellationToken = default) + { + var streamFlags = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); + var crc = await _reader + .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + var calcCrc = Crc32.Compute(streamFlags); + if (crc != calcCrc) + { + throw new InvalidFormatException("Stream header corrupt"); + } + + BlockCheckType = (CheckType)(streamFlags[1] & 0x0F); + var futureUse = (byte)(streamFlags[1] & 0xF0); + if (futureUse != 0 || streamFlags[0] != 0) + { + throw new InvalidFormatException("Unknown XZ Stream Version"); + } + } + private void CheckMagicBytes(byte[] header) { if (!header.SequenceEqual(MagicHeader)) diff --git a/src/SharpCompress/Compressors/Xz/XZIndex.cs b/src/SharpCompress/Compressors/Xz/XZIndex.cs index ac41732b2..9c5230911 100644 --- a/src/SharpCompress/Compressors/Xz/XZIndex.cs +++ b/src/SharpCompress/Compressors/Xz/XZIndex.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -39,6 +41,20 @@ public static XZIndex FromStream(Stream stream, bool indexMarkerAlreadyVerified) return index; } + public static async Task FromStreamAsync( + Stream stream, + bool indexMarkerAlreadyVerified, + CancellationToken cancellationToken = default + ) + { + var index = new XZIndex( + new BinaryReader(stream, Encoding.UTF8, true), + indexMarkerAlreadyVerified + ); + await index.ProcessAsync(cancellationToken).ConfigureAwait(false); + return index; + } + public void Process() { if (!_indexMarkerAlreadyVerified) @@ -55,6 +71,26 @@ public void Process() VerifyCrc32(); } + public async Task ProcessAsync(CancellationToken cancellationToken = default) + { + if (!_indexMarkerAlreadyVerified) + { + await VerifyIndexMarkerAsync(cancellationToken).ConfigureAwait(false); + } + + NumberOfRecords = await _reader.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false); + for (ulong i = 0; i < NumberOfRecords; i++) + { + Records.Add( + await XZIndexRecord + .FromBinaryReaderAsync(_reader, cancellationToken) + .ConfigureAwait(false) + ); + } + await SkipPaddingAsync(cancellationToken).ConfigureAwait(false); + await VerifyCrc32Async(cancellationToken).ConfigureAwait(false); + } + private void VerifyIndexMarker() { var marker = _reader.ReadByte(); @@ -64,6 +100,15 @@ private void VerifyIndexMarker() } } + private async Task VerifyIndexMarkerAsync(CancellationToken cancellationToken = default) + { + var marker = await _reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + if (marker != 0) + { + throw new InvalidFormatException("Not an index block"); + } + } + private void SkipPadding() { var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4; @@ -77,9 +122,32 @@ private void SkipPadding() } } + private async Task SkipPaddingAsync(CancellationToken cancellationToken = default) + { + var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4; + if (bytes > 0) + { + var paddingBytes = await _reader + .ReadBytesAsync(4 - bytes, cancellationToken) + .ConfigureAwait(false); + if (paddingBytes.Any(b => b != 0)) + { + throw new InvalidFormatException("Padding bytes were non-null"); + } + } + } + private void VerifyCrc32() { var crc = _reader.ReadLittleEndianUInt32(); // TODO verify this matches } + + private async Task VerifyCrc32Async(CancellationToken cancellationToken = default) + { + var crc = await _reader + .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + // TODO verify this matches + } } diff --git a/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs b/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs index e05b988bb..14e76eb86 100644 --- a/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs +++ b/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.Xz; @@ -18,4 +20,16 @@ public static XZIndexRecord FromBinaryReader(BinaryReader br) record.UncompressedSize = br.ReadXZInteger(); return record; } + + public static async Task FromBinaryReaderAsync( + BinaryReader br, + CancellationToken cancellationToken = default + ) + { + var record = new XZIndexRecord(); + record.UnpaddedSize = await br.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false); + record.UncompressedSize = await br.ReadXZIntegerAsync(cancellationToken) + .ConfigureAwait(false); + return record; + } } diff --git a/src/SharpCompress/Compressors/Xz/XZStream.cs b/src/SharpCompress/Compressors/Xz/XZStream.cs index 96f70dca5..ebd0924ed 100644 --- a/src/SharpCompress/Compressors/Xz/XZStream.cs +++ b/src/SharpCompress/Compressors/Xz/XZStream.cs @@ -2,6 +2,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -104,6 +106,35 @@ public override int Read(byte[] buffer, int offset, int count) return bytesRead; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var bytesRead = 0; + if (_endOfStream) + { + return bytesRead; + } + + if (!HeaderIsRead) + { + await ReadHeaderAsync(cancellationToken).ConfigureAwait(false); + } + + bytesRead = await ReadBlocksAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (bytesRead < count) + { + _endOfStream = true; + await ReadIndexAsync(cancellationToken).ConfigureAwait(false); + await ReadFooterAsync(cancellationToken).ConfigureAwait(false); + } + return bytesRead; + } + private void ReadHeader() { Header = XZHeader.FromStream(BaseStream); @@ -111,12 +142,31 @@ private void ReadHeader() HeaderIsRead = true; } + private async Task ReadHeaderAsync(CancellationToken cancellationToken = default) + { + Header = await XZHeader + .FromStreamAsync(BaseStream, cancellationToken) + .ConfigureAwait(false); + AssertBlockCheckTypeIsSupported(); + HeaderIsRead = true; + } + private void ReadIndex() => Index = XZIndex.FromStream(BaseStream, true); - // TODO veryfy Index + private async Task ReadIndexAsync(CancellationToken cancellationToken = default) => + Index = await XZIndex + .FromStreamAsync(BaseStream, true, cancellationToken) + .ConfigureAwait(false); + + // TODO verify Index private void ReadFooter() => Footer = XZFooter.FromStream(BaseStream); // TODO verify footer + private async Task ReadFooterAsync(CancellationToken cancellationToken = default) => + Footer = await XZFooter + .FromStreamAsync(BaseStream, cancellationToken) + .ConfigureAwait(false); + private int ReadBlocks(byte[] buffer, int offset, int count) { var bytesRead = 0; @@ -152,6 +202,48 @@ private int ReadBlocks(byte[] buffer, int offset, int count) return bytesRead; } + private async Task ReadBlocksAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var bytesRead = 0; + if (_currentBlock is null) + { + NextBlock(); + } + + for (; ; ) + { + try + { + if (bytesRead >= count) + { + break; + } + + var remaining = count - bytesRead; + var newOffset = offset + bytesRead; + var justRead = await _currentBlock + .ReadAsync(buffer, newOffset, remaining, cancellationToken) + .ConfigureAwait(false); + if (justRead < remaining) + { + NextBlock(); + } + + bytesRead += justRead; + } + catch (XZIndexMarkerReachedException) + { + break; + } + } + return bytesRead; + } + private void NextBlock() => _currentBlock = new XZBlock(BaseStream, Header.BlockCheckType, Header.BlockCheckSize); } diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs index 05ee7876a..d781c79b7 100644 --- a/src/SharpCompress/Utility.cs +++ b/src/SharpCompress/Utility.cs @@ -447,6 +447,31 @@ public static bool ReadFully(this Stream stream, Span buffer) } #endif + public static async Task ReadFullyAsync( + this Stream stream, + byte[] buffer, + CancellationToken cancellationToken = default + ) + { + var total = 0; + int read; + while ( + ( + read = await stream + .ReadAsync(buffer, total, buffer.Length - total, cancellationToken) + .ConfigureAwait(false) + ) > 0 + ) + { + total += read; + if (total >= buffer.Length) + { + return true; + } + } + return (total >= buffer.Length); + } + public static string TrimNulls(this string source) => source.Replace('\0', ' ').Trim(); /// diff --git a/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs new file mode 100644 index 000000000..8ccd569d8 --- /dev/null +++ b/tests/SharpCompress.Test/Xz/XZBlockAsyncTests.cs @@ -0,0 +1,125 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.Xz; +using Xunit; + +namespace SharpCompress.Test.Xz; + +public class XzBlockAsyncTests : XzTestsBase +{ + protected override void Rewind(Stream stream) => stream.Position = 12; + + protected override void RewindIndexed(Stream stream) => stream.Position = 12; + + private static async Task ReadBytesAsync(XZBlock block, int bytesToRead) + { + var buffer = new byte[bytesToRead]; + var read = await block.ReadAsync(buffer, 0, bytesToRead).ConfigureAwait(false); + if (read != bytesToRead) + { + throw new EndOfStreamException(); + } + + return buffer; + } + + [Fact] + public async Task OnFindIndexBlockThrowAsync() + { + var bytes = new byte[] { 0 }; + using Stream indexBlockStream = new MemoryStream(bytes); + var xzBlock = new XZBlock(indexBlockStream, CheckType.CRC64, 8); + await Assert.ThrowsAsync(async () => + { + await ReadBytesAsync(xzBlock, 1).ConfigureAwait(false); + }); + } + + [Fact] + public async Task CrcIncorrectThrowsAsync() + { + var bytes = (byte[])Compressed.Clone(); + bytes[20]++; + using Stream badCrcStream = new MemoryStream(bytes); + Rewind(badCrcStream); + var xzBlock = new XZBlock(badCrcStream, CheckType.CRC64, 8); + var ex = await Assert.ThrowsAsync(async () => + { + await ReadBytesAsync(xzBlock, 1).ConfigureAwait(false); + }); + Assert.Equal("Block header corrupt", ex.Message); + } + + [Fact] + public async Task CanReadMAsync() + { + var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); + Assert.Equal( + Encoding.ASCII.GetBytes("M"), + await ReadBytesAsync(xzBlock, 1).ConfigureAwait(false) + ); + } + + [Fact] + public async Task CanReadMaryAsync() + { + var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); + Assert.Equal( + Encoding.ASCII.GetBytes("M"), + await ReadBytesAsync(xzBlock, 1).ConfigureAwait(false) + ); + Assert.Equal( + Encoding.ASCII.GetBytes("a"), + await ReadBytesAsync(xzBlock, 1).ConfigureAwait(false) + ); + Assert.Equal( + Encoding.ASCII.GetBytes("ry"), + await ReadBytesAsync(xzBlock, 2).ConfigureAwait(false) + ); + } + + [Fact] + public async Task CanReadPoemWithStreamReaderAsync() + { + var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); + var sr = new StreamReader(xzBlock); + Assert.Equal(await sr.ReadToEndAsync().ConfigureAwait(false), Original); + } + + [Fact] + public async Task NoopWhenNoPaddingAsync() + { + // CompressedStream's only block has no padding. + var xzBlock = new XZBlock(CompressedStream, CheckType.CRC64, 8); + var sr = new StreamReader(xzBlock); + await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(0L, CompressedStream.Position % 4L); + } + + [Fact] + public async Task SkipsPaddingWhenPresentAsync() + { + // CompressedIndexedStream's first block has 1-byte padding. + var xzBlock = new XZBlock(CompressedIndexedStream, CheckType.CRC64, 8); + var sr = new StreamReader(xzBlock); + await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(0L, CompressedIndexedStream.Position % 4L); + } + + [Fact] + public async Task HandlesPaddingInUnalignedBlockAsync() + { + var compressedUnaligned = new byte[Compressed.Length + 1]; + Compressed.CopyTo(compressedUnaligned, 1); + var compressedUnalignedStream = new MemoryStream(compressedUnaligned); + compressedUnalignedStream.Position = 13; + + // Compressed's only block has no padding. + var xzBlock = new XZBlock(compressedUnalignedStream, CheckType.CRC64, 8); + var sr = new StreamReader(xzBlock); + await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(1L, compressedUnalignedStream.Position % 4L); + } +} diff --git a/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs new file mode 100644 index 000000000..5fc11c39c --- /dev/null +++ b/tests/SharpCompress.Test/Xz/XZHeaderAsyncTests.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.Xz; +using Xunit; + +namespace SharpCompress.Test.Xz; + +public class XzHeaderAsyncTests : XzTestsBase +{ + [Fact] + public async Task ChecksMagicNumberAsync() + { + var bytes = (byte[])Compressed.Clone(); + bytes[3]++; + using Stream badMagicNumberStream = new MemoryStream(bytes); + var br = new BinaryReader(badMagicNumberStream); + var header = new XZHeader(br); + var ex = await Assert.ThrowsAsync(async () => + { + await header.ProcessAsync().ConfigureAwait(false); + }); + Assert.Equal("Invalid XZ Stream", ex.Message); + } + + [Fact] + public async Task CorruptHeaderThrowsAsync() + { + var bytes = (byte[])Compressed.Clone(); + bytes[8]++; + using Stream badCrcStream = new MemoryStream(bytes); + var br = new BinaryReader(badCrcStream); + var header = new XZHeader(br); + var ex = await Assert.ThrowsAsync(async () => + { + await header.ProcessAsync().ConfigureAwait(false); + }); + Assert.Equal("Stream header corrupt", ex.Message); + } + + [Fact] + public async Task BadVersionIfCrcOkButStreamFlagUnknownAsync() + { + var bytes = (byte[])Compressed.Clone(); + byte[] streamFlags = [0x00, 0xF4]; + var crc = Crc32.Compute(streamFlags).ToLittleEndianBytes(); + streamFlags.CopyTo(bytes, 6); + crc.CopyTo(bytes, 8); + using Stream badFlagStream = new MemoryStream(bytes); + var br = new BinaryReader(badFlagStream); + var header = new XZHeader(br); + var ex = await Assert.ThrowsAsync(async () => + { + await header.ProcessAsync().ConfigureAwait(false); + }); + Assert.Equal("Unknown XZ Stream Version", ex.Message); + } + + [Fact] + public async Task ProcessesBlockCheckTypeAsync() + { + var br = new BinaryReader(CompressedStream); + var header = new XZHeader(br); + await header.ProcessAsync().ConfigureAwait(false); + Assert.Equal(CheckType.CRC64, header.BlockCheckType); + } + + [Fact] + public async Task CanCalculateBlockCheckSizeAsync() + { + var br = new BinaryReader(CompressedStream); + var header = new XZHeader(br); + await header.ProcessAsync().ConfigureAwait(false); + Assert.Equal(8, header.BlockCheckSize); + } + + [Fact] + public async Task ProcessesStreamHeaderFromFactoryAsync() + { + var header = await XZHeader.FromStreamAsync(CompressedStream).ConfigureAwait(false); + Assert.Equal(CheckType.CRC64, header.BlockCheckType); + } +} diff --git a/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs new file mode 100644 index 000000000..9b6805b31 --- /dev/null +++ b/tests/SharpCompress.Test/Xz/XZIndexAsyncTests.cs @@ -0,0 +1,85 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.Xz; +using Xunit; + +namespace SharpCompress.Test.Xz; + +public class XzIndexAsyncTests : XzTestsBase +{ + protected override void RewindEmpty(Stream stream) => stream.Position = 12; + + protected override void Rewind(Stream stream) => stream.Position = 356; + + protected override void RewindIndexed(Stream stream) => stream.Position = 612; + + [Fact] + public void RecordsStreamStartOnInit() + { + using Stream badStream = new MemoryStream([1, 2, 3, 4, 5]); + var br = new BinaryReader(badStream); + var index = new XZIndex(br, false); + Assert.Equal(0, index.StreamStartPosition); + } + + [Fact] + public async Task ThrowsIfHasNoIndexMarkerAsync() + { + using Stream badStream = new MemoryStream([1, 2, 3, 4, 5]); + var br = new BinaryReader(badStream); + var index = new XZIndex(br, false); + await Assert.ThrowsAsync(async () => + await index.ProcessAsync().ConfigureAwait(false) + ); + } + + [Fact] + public async Task ReadsNoRecordAsync() + { + var br = new BinaryReader(CompressedEmptyStream); + var index = new XZIndex(br, false); + await index.ProcessAsync().ConfigureAwait(false); + Assert.Equal((ulong)0, index.NumberOfRecords); + } + + [Fact] + public async Task ReadsOneRecordAsync() + { + var br = new BinaryReader(CompressedStream); + var index = new XZIndex(br, false); + await index.ProcessAsync().ConfigureAwait(false); + Assert.Equal((ulong)1, index.NumberOfRecords); + } + + [Fact] + public async Task ReadsMultipleRecordsAsync() + { + var br = new BinaryReader(CompressedIndexedStream); + var index = new XZIndex(br, false); + await index.ProcessAsync().ConfigureAwait(false); + Assert.Equal((ulong)2, index.NumberOfRecords); + } + + [Fact] + public async Task ReadsFirstRecordAsync() + { + var br = new BinaryReader(CompressedStream); + var index = new XZIndex(br, false); + await index.ProcessAsync().ConfigureAwait(false); + Assert.Equal((ulong)OriginalBytes.Length, index.Records[0].UncompressedSize); + } + + [Fact] + public async Task SkipsPaddingAsync() + { + // Index with 3-byte padding. + using Stream badStream = new MemoryStream( + [0x00, 0x01, 0x10, 0x80, 0x01, 0x00, 0x00, 0x00, 0xB1, 0x01, 0xD9, 0xC9, 0xFF] + ); + var br = new BinaryReader(badStream); + var index = new XZIndex(br, false); + await index.ProcessAsync().ConfigureAwait(false); + Assert.Equal(0L, badStream.Position % 4L); + } +} diff --git a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs new file mode 100644 index 000000000..7d4ba386c --- /dev/null +++ b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Compressors.Xz; +using Xunit; + +namespace SharpCompress.Test.Xz; + +public class XzStreamAsyncTests : XzTestsBase +{ + [Fact] + public async Task CanReadEmptyStreamAsync() + { + var xz = new XZStream(CompressedEmptyStream); + using var sr = new StreamReader(xz); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(OriginalEmpty, uncompressed); + } + + [Fact] + public async Task CanReadStreamAsync() + { + var xz = new XZStream(CompressedStream); + using var sr = new StreamReader(xz); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(Original, uncompressed); + } + + [Fact] + public async Task CanReadIndexedStreamAsync() + { + var xz = new XZStream(CompressedIndexedStream); + using var sr = new StreamReader(xz); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(OriginalIndexed, uncompressed); + } +}