diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index c65cc6834..276456fba 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -153,7 +153,7 @@ public void CopyPending() _pendingLen = rem; } - public async Task CopyPendingAsync(CancellationToken cancellationToken = default) + public async ValueTask CopyPendingAsync(CancellationToken cancellationToken = default) { if (_pendingLen < 1) { @@ -206,7 +206,7 @@ public void CopyBlock(int distance, int len) _pendingDist = distance; } - public async Task CopyBlockAsync( + public async ValueTask CopyBlockAsync( int distance, int len, CancellationToken cancellationToken = default @@ -253,7 +253,7 @@ public void PutByte(byte b) } } - public async Task PutByteAsync(byte b, CancellationToken cancellationToken = default) + public async ValueTask PutByteAsync(byte b, CancellationToken cancellationToken = default) { _buffer[_pos++] = b; _total++; @@ -369,6 +369,28 @@ public int Read(byte[] buffer, int offset, int count) return size; } + public int Read(Memory buffer, int offset, int count) + { + if (_streamPos >= _pos) + { + return 0; + } + + var size = _pos - _streamPos; + if (size > count) + { + size = count; + } + _buffer.AsMemory(_streamPos, size).CopyTo(buffer.Slice(offset, size)); + _streamPos += size; + if (_streamPos >= _windowSize) + { + _pos = 0; + _streamPos = 0; + } + return size; + } + public int ReadByte() { if (_streamPos >= _pos) diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs index fed40533d..95d3d027b 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading.Tasks; using SharpCompress.Compressors.LZMA.LZ; using SharpCompress.Compressors.LZMA.RangeCoder; @@ -475,7 +476,7 @@ internal bool Code(int dictionarySize, OutWindow outWindow, RangeCoder.Decoder r return false; } - internal async System.Threading.Tasks.Task CodeAsync( + internal async ValueTask CodeAsync( int dictionarySize, OutWindow outWindow, RangeCoder.Decoder rangeDecoder, diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index e9d5877ff..77e4c4947 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -425,7 +425,7 @@ private void DecodeChunkHeader() } } - private async Task DecodeChunkHeaderAsync(CancellationToken cancellationToken = default) + private async ValueTask DecodeChunkHeaderAsync(CancellationToken cancellationToken = default) { var controlBuffer = new byte[1]; await _inputStream @@ -632,6 +632,119 @@ await _decoder return total; } +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_endReached) + { + return 0; + } + + var total = 0; + var offset = 0; + var count = buffer.Length; + while (total < count) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_availableBytes == 0) + { + if (_isLzma2) + { + await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _endReached = true; + } + if (_endReached) + { + break; + } + } + + var toProcess = count - total; + if (toProcess > _availableBytes) + { + toProcess = (int)_availableBytes; + } + + _outWindow.SetLimit(toProcess); + if (_uncompressedChunk) + { + _inputPosition += await _outWindow + .CopyStreamAsync(_inputStream, toProcess, cancellationToken) + .ConfigureAwait(false); + } + else if ( + await _decoder + .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) + .ConfigureAwait(false) + && _outputSize < 0 + ) + { + _availableBytes = _outWindow.AvailableBytes; + } + + var read = _outWindow.Read(buffer, offset, toProcess); + total += read; + offset += read; + _position += read; + _availableBytes -= read; + + if (_availableBytes == 0 && !_uncompressedChunk) + { + if ( + !_rangeDecoder.IsFinished + || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit) + ) + { + _outWindow.SetLimit(toProcess + 1); + if ( + !await _decoder + .CodeAsync( + _dictionarySize, + _outWindow, + _rangeDecoder, + cancellationToken + ) + .ConfigureAwait(false) + ) + { + _rangeDecoder.ReleaseStream(); + throw new DataErrorException(); + } + } + + _rangeDecoder.ReleaseStream(); + + _inputPosition += _rangeDecoder._total; + if (_outWindow.HasPending) + { + throw new DataErrorException(); + } + } + } + + if (_endReached) + { + if (_inputSize >= 0 && _inputPosition != _inputSize) + { + throw new DataErrorException(); + } + if (_outputSize >= 0 && _position != _outputSize) + { + throw new DataErrorException(); + } + } + + return total; + } +#endif + public override Task WriteAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/IO/BufferedSubStream.cs b/src/SharpCompress/IO/BufferedSubStream.cs index d719fd907..f9216216e 100755 --- a/src/SharpCompress/IO/BufferedSubStream.cs +++ b/src/SharpCompress/IO/BufferedSubStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.IO; @@ -68,6 +70,23 @@ private void RefillCache() BytesLeftToRead -= _cacheLength; } + private async ValueTask RefillCacheAsync(CancellationToken cancellationToken) + { + var count = (int)Math.Min(BytesLeftToRead, _cache.Length); + _cacheOffset = 0; + if (count == 0) + { + _cacheLength = 0; + return; + } + Stream.Position = origin; + _cacheLength = await Stream + .ReadAsync(_cache, 0, count, cancellationToken) + .ConfigureAwait(false); + origin += _cacheLength; + BytesLeftToRead -= _cacheLength; + } + public override int Read(byte[] buffer, int offset, int count) { if (count > Length) @@ -104,6 +123,61 @@ public override int ReadByte() return _cache[_cacheOffset++]; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (count > Length) + { + count = (int)Length; + } + + if (count > 0) + { + if (_cacheOffset == _cacheLength) + { + await RefillCacheAsync(cancellationToken).ConfigureAwait(false); + } + + count = Math.Min(count, _cacheLength - _cacheOffset); + Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count); + _cacheOffset += count; + } + + return count; + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var count = buffer.Length; + if (count > Length) + { + count = (int)Length; + } + + if (count > 0) + { + if (_cacheOffset == _cacheLength) + { + await RefillCacheAsync(cancellationToken).ConfigureAwait(false); + } + + count = Math.Min(count, _cacheLength - _cacheOffset); + _cache.AsSpan(_cacheOffset, count).CopyTo(buffer.Span); + _cacheOffset += count; + } + + return count; + } +#endif + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs new file mode 100644 index 000000000..0029105c0 --- /dev/null +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs @@ -0,0 +1,139 @@ +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Common; +using Xunit; + +namespace SharpCompress.Test.SevenZip; + +#if !NETFRAMEWORK +public class SevenZipArchiveAsyncTests : ArchiveTests +{ + [Fact] + public async Task SevenZipArchive_LZMA_AsyncStreamExtraction() + { + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z"); + using var stream = File.OpenRead(testArchive); + using var archive = ArchiveFactory.Open(stream); + + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!); + var targetDir = Path.GetDirectoryName(targetPath); + + if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var targetStream = File.Create(targetPath); + await sourceStream.CopyToAsync(targetStream, CancellationToken.None); + } + + VerifyFiles(); + } + + [Fact] + public async Task SevenZipArchive_LZMA2_AsyncStreamExtraction() + { + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA2.7z"); + using var stream = File.OpenRead(testArchive); + using var archive = ArchiveFactory.Open(stream); + + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!); + var targetDir = Path.GetDirectoryName(targetPath); + + if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var targetStream = File.Create(targetPath); + await sourceStream.CopyToAsync(targetStream, CancellationToken.None); + } + + VerifyFiles(); + } + + [Fact] + public async Task SevenZipArchive_Solid_AsyncStreamExtraction() + { + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z"); + using var stream = File.OpenRead(testArchive); + using var archive = ArchiveFactory.Open(stream); + + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!); + var targetDir = Path.GetDirectoryName(targetPath); + + if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var targetStream = File.Create(targetPath); + await sourceStream.CopyToAsync(targetStream, CancellationToken.None); + } + + VerifyFiles(); + } + + [Fact] + public async Task SevenZipArchive_BZip2_AsyncStreamExtraction() + { + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.BZip2.7z"); + using var stream = File.OpenRead(testArchive); + using var archive = ArchiveFactory.Open(stream); + + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!); + var targetDir = Path.GetDirectoryName(targetPath); + + if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var targetStream = File.Create(targetPath); + await sourceStream.CopyToAsync(targetStream, CancellationToken.None); + } + + VerifyFiles(); + } + + [Fact] + public async Task SevenZipArchive_PPMd_AsyncStreamExtraction() + { + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.PPMd.7z"); + using var stream = File.OpenRead(testArchive); + using var archive = ArchiveFactory.Open(stream); + + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!); + var targetDir = Path.GetDirectoryName(targetPath); + + if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var targetStream = File.Create(targetPath); + await sourceStream.CopyToAsync(targetStream, CancellationToken.None); + } + + VerifyFiles(); + } +} +#endif