From 06a70fdd1e704a75f2b41f6005cc3f7d288e961f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 29 Apr 2026 11:45:45 +0100 Subject: [PATCH] Fully implement FInishAsync --- .../Compressors/BZip2/BZip2Stream.cs | 2 + .../Compressors/BZip2/CBZip2OutputStream.cs | 2 +- .../Compressors/LZMA/LZipStream.Async.cs | 77 +++++++++++++++++++ .../Default/LZipCompressionProvider.cs | 15 ++++ src/SharpCompress/Providers/IFinishable.cs | 4 + .../Writers/Tar/TarWriter.Async.cs | 2 +- .../Mocks/AsyncOnlyStream.cs | 3 + .../Tar/TarWriterAsyncTests.cs | 2 +- 8 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs index 0813b088..995031f6 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs @@ -49,6 +49,8 @@ public sealed partial class BZip2Stream : Stream, IFinishable return bZip2Stream; } + public ValueTask FinishAsync() => (stream as CBZip2OutputStream)?.FinishAsync() ?? default; + public void Finish() => (stream as CBZip2OutputStream)?.Finish(); protected override void Dispose(bool disposing) diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs index 9b50f502..04d5581d 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs @@ -2088,7 +2088,7 @@ internal sealed class CBZip2OutputStream : Stream /// Asynchronously finalizes the BZip2 compressed stream, flushing all pending data. /// Writes the remaining compressed data to the underlying stream using async I/O. /// - public async Task FinishAsync(CancellationToken cancellationToken = default) + public async ValueTask FinishAsync(CancellationToken cancellationToken = default) { if (finished) { diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs index 0a85a378..657f4a53 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Binary; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -9,6 +10,82 @@ namespace SharpCompress.Compressors.LZMA; public sealed partial class LZipStream { + public static ValueTask CreateAsync( + Stream stream, + CompressionMode mode, + bool leaveOpen = false, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (mode != CompressionMode.Compress) + { + return new ValueTask(new LZipStream(stream, mode, leaveOpen)); + } + + // The LZMA encoder used for LZip currently finalizes synchronously, so the async + // creation path compresses to memory and writes the completed member asynchronously. + var compressedBuffer = new MemoryStream(); + return new ValueTask( + new LZipStream(compressedBuffer, mode, leaveOpen, stream, compressedBuffer) + ); + } + + public async ValueTask FinishAsync() + { + if (_finished) + { + return; + } + + if (Mode == CompressionMode.Compress) + { + var crc32Stream = (Crc32Stream)_stream; + FinishWrappedStream(crc32Stream); + var compressedCount = _countingWritableSubStream.NotNull().BytesWritten; + + var intBuf = new byte[8]; + BinaryPrimitives.WriteUInt32LittleEndian(intBuf, crc32Stream.Crc); + await _countingWritableSubStream + .NotNull() + .WriteAsync(intBuf, 0, 4, CancellationToken.None) + .ConfigureAwait(false); + + BinaryPrimitives.WriteInt64LittleEndian(intBuf, _writeCount); + await _countingWritableSubStream + .NotNull() + .WriteAsync(intBuf, 0, intBuf.Length, CancellationToken.None) + .ConfigureAwait(false); + + // Total member size includes the 6-byte header and 20-byte trailer. + BinaryPrimitives.WriteUInt64LittleEndian( + intBuf, + (ulong)compressedCount + (ulong)(6 + 20) + ); + await _countingWritableSubStream + .NotNull() + .WriteAsync(intBuf, 0, intBuf.Length, CancellationToken.None) + .ConfigureAwait(false); + + if (_asyncCompressedBuffer is not null && _asyncFinalDestination is not null) + { + _asyncCompressedBuffer.Position = 0; + await _asyncCompressedBuffer + .CopyToAsync(_asyncFinalDestination, 81920, CancellationToken.None) + .ConfigureAwait(false); + } + } + + _finished = true; + } + + private static void FinishWrappedStream(Crc32Stream crc32Stream) + { + crc32Stream.WrappedStream.Dispose(); + crc32Stream.Dispose(); + } + /// /// Asynchronously determines if the given stream is positioned at the start of a v1 LZip /// file, as indicated by the ASCII characters "LZIP" and a version byte diff --git a/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs index c4fe48b6..e5bf4f6f 100644 --- a/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Compressors; using SharpCompress.Compressors.LZMA; @@ -19,6 +21,19 @@ public sealed class LZipCompressionProvider : CompressionProviderBase return new LZipStream(destination, CompressionMode.Compress); } + public override async ValueTask CreateCompressStreamAsync( + Stream destination, + int compressionLevel, + CancellationToken cancellationToken = default + ) => + await LZipStream + .CreateAsync( + destination, + CompressionMode.Compress, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); + public override Stream CreateDecompressStream(Stream source) { return new LZipStream(source, CompressionMode.Decompress); diff --git a/src/SharpCompress/Providers/IFinishable.cs b/src/SharpCompress/Providers/IFinishable.cs index 53173d53..ea4980d4 100644 --- a/src/SharpCompress/Providers/IFinishable.cs +++ b/src/SharpCompress/Providers/IFinishable.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + namespace SharpCompress.Providers; /// @@ -16,4 +18,6 @@ public interface IFinishable /// and writing format-specific trailer/footer bytes. /// void Finish(); + + ValueTask FinishAsync(); } diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs index 20b6a8fe..86e12c43 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs @@ -29,7 +29,7 @@ public partial class TarWriter } if (OutputStream is IFinishable finishable) { - finishable.Finish(); + await finishable.FinishAsync().ConfigureAwait(false); } OutputStream?.Dispose(); // base.DisposeAsync() is a no-op since _isDisposed is already set diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs index 937df54d..823327f0 100644 --- a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -19,6 +19,9 @@ public class AsyncOnlyStream(Stream stream) : Stream set => _stream.Position = value; } + public override Task FlushAsync(CancellationToken cancellationToken) => + _stream.FlushAsync(cancellationToken); + public override void Flush() => throw new NotSupportedException("Synchronous Flush is not supported"); diff --git a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs index d9884543..8f6c5079 100644 --- a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs @@ -65,7 +65,7 @@ public class TarWriterAsyncTests : WriterTests { using var stream = new MemoryStream(); using Stream content = File.OpenRead(Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg")); - using ( + await using ( var writer = new TarWriter( new AsyncOnlyStream(stream), new TarWriterOptions(CompressionType.None, finalizeArchive)