Merge pull request #1004 from adamhathcock/adam/async-xz

Async XZ
This commit is contained in:
Adam Hathcock
2025-11-19 10:53:24 +00:00
committed by GitHub
13 changed files with 823 additions and 2 deletions

View File

@@ -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 class BinaryUtils
internal static uint ReadLittleEndianUInt32(this Stream stream) =>
unchecked((uint)ReadLittleEndianInt32(stream));
public static async Task<int> 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<uint> 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);

View File

@@ -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 @@ internal static class MultiByteIntegers
}
return Output;
}
public static async Task<ulong> 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<byte> 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<byte[]> 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;
}
}

View File

@@ -4,6 +4,8 @@ using System;
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 sealed class XZBlock : XZReadOnlyStream
return bytesRead;
}
public override async Task<int> 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 @@ public sealed class XZBlock : XZReadOnlyStream
_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 @@ public sealed class XZBlock : XZReadOnlyStream
_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 @@ public sealed class XZBlock : XZReadOnlyStream
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 @@ public sealed class XZBlock : XZReadOnlyStream
}
}
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 @@ public sealed class XZBlock : XZReadOnlyStream
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 @@ public sealed class XZBlock : XZReadOnlyStream
return blockHeaderWithoutCrc;
}
private async Task<byte[]> 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();

View File

@@ -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 class XZFooter
return footer;
}
public static async Task<XZFooter> 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 class XZFooter
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");
}
}
}

View File

@@ -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 class XZHeader
return header;
}
public static async Task<XZHeader> 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 @@ public class XZHeader
}
}
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))

View File

@@ -3,6 +3,8 @@ using System.Collections.Generic;
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 class XZIndex
return index;
}
public static async Task<XZIndex> 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 class XZIndex
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 @@ public class XZIndex
}
}
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 @@ public class XZIndex
}
}
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
}
}

View File

@@ -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 class XZIndexRecord
record.UncompressedSize = br.ReadXZInteger();
return record;
}
public static async Task<XZIndexRecord> 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;
}
}

View File

@@ -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 sealed class XZStream : XZReadOnlyStream, IStreamStack
return bytesRead;
}
public override async Task<int> 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 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack
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 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack
return bytesRead;
}
private async Task<int> 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);
}

View File

@@ -447,6 +447,31 @@ internal static class Utility
}
#endif
public static async Task<bool> 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();
/// <summary>

View File

@@ -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<byte[]> 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<XZIndexMarkerReachedException>(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<InvalidFormatException>(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);
}
}

View File

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

View File

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

View File

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