From 2175cb299d928af4e4152f6ee54a624ca2605bb6 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 20 Jan 2026 12:22:38 +0000 Subject: [PATCH] tar fixes --- src/SharpCompress/Common/Tar/TarEntry.cs | 26 +++++++++++ .../Compressors/Deflate/DeflateStream.cs | 34 +++++++------- src/SharpCompress/IO/SharpCompressStream.cs | 45 ++++++++++++++++--- src/SharpCompress/Readers/Tar/TarReader.cs | 7 +++ .../Mocks/AsyncOnlyStream.cs | 5 --- .../Mocks/FlushOnDisposeStream.cs | 10 +++++ tests/SharpCompress.Test/Mocks/TestStream.cs | 8 ++++ tests/SharpCompress.Test/ReaderTests.cs | 16 ++++++- .../Tar/TarReaderAsyncTests.cs | 19 +++++--- 9 files changed, 131 insertions(+), 39 deletions(-) diff --git a/src/SharpCompress/Common/Tar/TarEntry.cs b/src/SharpCompress/Common/Tar/TarEntry.cs index 9dc5e471..33023894 100644 --- a/src/SharpCompress/Common/Tar/TarEntry.cs +++ b/src/SharpCompress/Common/Tar/TarEntry.cs @@ -76,4 +76,30 @@ public class TarEntry : Entry } } } + internal static async IAsyncEnumerable GetEntriesAsync( + StreamingMode mode, + Stream stream, + CompressionType compressionType, + IArchiveEncoding archiveEncoding + ) + { + await foreach (var header in TarHeaderFactory.ReadHeaderAsync(mode, stream, archiveEncoding)) + { + if (header != null) + { + if (mode == StreamingMode.Seekable) + { + yield return new TarEntry(new TarFilePart(header, stream), compressionType); + } + else + { + yield return new TarEntry(new TarFilePart(header, null), compressionType); + } + } + else + { + throw new IncompleteArchiveException("Unexpected EOF reading tar file"); + } + } + } } diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.cs index 55c24813..6e8b0fe7 100644 --- a/src/SharpCompress/Compressors/Deflate/DeflateStream.cs +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.cs @@ -278,6 +278,22 @@ public class DeflateStream : Stream, IStreamStack } } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask DisposeAsync() + { + if (!_disposed) + { +#if DEBUG_STREAMS + this.DebugDispose(typeof(DeflateStream)); +#endif + await _baseStream.DisposeAsync().ConfigureAwait(false); + _disposed = true; + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif + /// /// Flush the stream. /// @@ -299,24 +315,6 @@ public class DeflateStream : Stream, IStreamStack await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); } -#if !NETFRAMEWORK && !NETSTANDARD2_0 - public override async ValueTask DisposeAsync() - { - if (_disposed) - { - return; - } - _disposed = true; - if (_baseStream != null) - { - await _baseStream.DisposeAsync().ConfigureAwait(false); - } -#if DEBUG_STREAMS - this.DebugDispose(typeof(DeflateStream)); -#endif - await base.DisposeAsync().ConfigureAwait(false); - } -#endif /// /// Read data from the stream. diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 948fdb57..3c46732b 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -141,23 +141,23 @@ public class SharpCompressStream : Stream, IStreamStack #if DEBUG_STREAMS this.DebugDispose(typeof(SharpCompressStream)); #endif - if (_isDisposed) - { - return; - } - _isDisposed = true; - base.Dispose(disposing); - if (this.LeaveOpen) { return; } + if (ThrowOnDispose) { throw new InvalidOperationException( $"Attempt to dispose of a {nameof(SharpCompressStream)} when {nameof(ThrowOnDispose)} is {ThrowOnDispose}" ); } + if (_isDisposed) + { + return; + } + _isDisposed = true; + base.Dispose(disposing); if (disposing) { Stream.Dispose(); @@ -450,5 +450,36 @@ public class SharpCompressStream : Stream, IStreamStack _internalPosition += buffer.Length; } + public override async ValueTask DisposeAsync() + { +#if DEBUG_STREAMS + this.DebugDispose(typeof(SharpCompressStream)); +#endif + if (this.LeaveOpen) + { + return; + } + if (ThrowOnDispose) + { + throw new InvalidOperationException( + $"Attempt to dispose of a {nameof(SharpCompressStream)} when {nameof(ThrowOnDispose)} is {ThrowOnDispose}" + ); + } + if (_isDisposed) + { + return; + } + _isDisposed = true; + await base.DisposeAsync(); + + + await Stream.DisposeAsync(); + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null; + } + } + #endif } diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index a0d83a38..6be8b1b4 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -119,4 +119,11 @@ public partial class TarReader : AbstractReader compressionType, Options.ArchiveEncoding ); + protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => + TarEntry.GetEntriesAsync( + StreamingMode.Streaming, + stream, + compressionType, + Options.ArchiveEncoding + ); } diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs index f8e1136d..b4b148be 100644 --- a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -63,9 +63,4 @@ public class AsyncOnlyStream : SharpCompressStream public override void Write(byte[] buffer, int offset, int count) => Stream.Write(buffer, offset, count); - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - } } diff --git a/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs b/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs index 56cd0161..6f24ac64 100644 --- a/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs +++ b/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Test.Mocks; @@ -46,4 +47,13 @@ public class FlushOnDisposeStream(Stream innerStream) : Stream base.Dispose(disposing); } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask DisposeAsync() + { + await innerStream.FlushAsync(); + innerStream.Close(); + await base.DisposeAsync(); + } +#endif } diff --git a/tests/SharpCompress.Test/Mocks/TestStream.cs b/tests/SharpCompress.Test/Mocks/TestStream.cs index 37e1e808..ad0105c6 100644 --- a/tests/SharpCompress.Test/Mocks/TestStream.cs +++ b/tests/SharpCompress.Test/Mocks/TestStream.cs @@ -50,6 +50,14 @@ public class TestStream(Stream stream, bool read, bool write, bool seek) : Strea Memory buffer, CancellationToken cancellationToken = default ) => stream.ReadAsync(buffer, cancellationToken); + + + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + await stream.DisposeAsync(); + IsDisposed = true; + } #endif public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 54399ee1..f3145e61 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -160,13 +160,25 @@ public abstract class ReaderTests : TestBase ) { using var file = File.OpenRead(testArchive); - using var protectedStream = SharpCompressStream.Create( + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + await using var protectedStream = SharpCompressStream.Create( new ForwardOnlyStream(file, options.BufferSize), leaveOpen: true, throwOnDispose: true, bufferSize: options.BufferSize ); - using var testStream = new TestStream(protectedStream); + await using var testStream = new TestStream(protectedStream); +#else + + using var protectedStream = SharpCompressStream.Create( + new ForwardOnlyStream(file, options.BufferSize), + leaveOpen: true, + throwOnDispose: true, + bufferSize: options.BufferSize + ); + using var testStream = new TestStream(protectedStream); + #endif await using ( var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(testStream), diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 45b80437..ee8e7942 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -163,20 +163,25 @@ public class TarReaderAsyncTests : ReaderTests } [Fact] - public void Tar_With_TarGz_With_Flushed_EntryStream_Async() + public async ValueTask Tar_With_TarGz_With_Flushed_EntryStream_Async() { var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsTarGz.tar"); using Stream stream = File.OpenRead(archiveFullPath); - using var reader = ReaderFactory.OpenReader(stream); - Assert.True(reader.MoveToNextEntry()); + await using var reader = await ReaderFactory.OpenAsyncReader(stream); + Assert.True(await reader.MoveToNextEntryAsync()); Assert.Equal("inner.tar.gz", reader.Entry.Key); - using var entryStream = reader.OpenEntryStream(); - using var flushingStream = new FlushOnDisposeStream(entryStream); +#if !NETFRAMEWORK && !NETSTANDARD2_0 + await using var entryStream = await reader.OpenEntryStreamAsync(); + await using var flushingStream = new FlushOnDisposeStream(entryStream); +#else + using var entryStream = reader.OpenEntryStream(); + using var flushingStream = new FlushOnDisposeStream(entryStream); + #endif // Extract inner.tar.gz - using var innerReader = ReaderFactory.OpenReader(flushingStream); - Assert.True(innerReader.MoveToNextEntry()); + await using var innerReader = await ReaderFactory.OpenAsyncReader(flushingStream); + Assert.True(await innerReader.MoveToNextEntryAsync()); Assert.Equal("test", innerReader.Entry.Key); }