diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index ab6b50a2..e640017b 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -182,10 +182,10 @@ public class TarFactory if (await wrapper.IsMatchAsync(stream, cancellationToken).ConfigureAwait(false)) { stream.Seek(0, SeekOrigin.Begin); -#if LEGACY_DOTNET && !NETSTANDARD2_1 - using var decompressedStream = await CreateProbeDecompressionStreamAsync( -#else +#if STREAM_DISPOSEASYNC await using var decompressedStream = await CreateProbeDecompressionStreamAsync( +#else + using var decompressedStream = await CreateProbeDecompressionStreamAsync( #endif stream, wrapper.CompressionType, @@ -397,10 +397,10 @@ public class TarFactory } catch { -#if LEGACY_DOTNET && !NETSTANDARD2_1 - buffered.Dispose(); -#else +#if STREAM_DISPOSEASYNC await buffered.DisposeAsync().ConfigureAwait(false); +#else + buffered.Dispose(); #endif throw; } @@ -484,10 +484,10 @@ public class TarFactory ) .ConfigureAwait(false); } -#if LEGACY_DOTNET && !NETSTANDARD2_1 - using var decompressedStream = await CreateProbeDecompressionStreamAsync( -#else +#if STREAM_DISPOSEASYNC await using var decompressedStream = await CreateProbeDecompressionStreamAsync( +#else + using var decompressedStream = await CreateProbeDecompressionStreamAsync( #endif sharpCompressStream, wrapper.CompressionType, @@ -522,10 +522,10 @@ public class TarFactory { if (!transferred) { -#if LEGACY_DOTNET && !NETSTANDARD2_1 - sharpCompressStream.Dispose(); -#else +#if STREAM_DISPOSEASYNC await sharpCompressStream.DisposeAsync().ConfigureAwait(false); +#else + sharpCompressStream.Dispose(); #endif } } diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index 6142d41b..5c9cc3b9 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -108,59 +108,91 @@ public partial class TarReader TarWrapper.MaximumRewindBufferSize ) ); - if (!sharpCompressStream.IsRecording) + var transferred = false; + try { - sharpCompressStream.StartRecording(); - } - long pos = sharpCompressStream.Position; - foreach (var wrapper in TarWrapper.Wrappers) - { - sharpCompressStream.Position = pos; - if ( - !await wrapper - .IsMatchAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false) - ) + if (!sharpCompressStream.IsRecording) { - continue; + sharpCompressStream.StartRecording(); } - - sharpCompressStream.Position = pos; - if (wrapper.CompressionType == CompressionType.LZip) - { - sharpCompressStream = await TarFactory - .BufferLZipHeaderAsync(sharpCompressStream, cancellationToken) - .ConfigureAwait(false); - pos = sharpCompressStream.Position; - } -#if LEGACY_DOTNET && !NETSTANDARD2_1 - using var testStream = await CreateProbeDecompressionStreamAsync( -#else - await using var testStream = await CreateProbeDecompressionStreamAsync( -#endif - sharpCompressStream, - wrapper.CompressionType, - readerOptions.Providers, - readerOptions, - cancellationToken - ) - .ConfigureAwait(false); - if ( - await TarArchive.IsTarFileAsync(testStream, cancellationToken).ConfigureAwait(false) - ) + long pos = sharpCompressStream.Position; + foreach (var wrapper in TarWrapper.Wrappers) { sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, readerOptions, wrapper.CompressionType); + if ( + !await wrapper + .IsMatchAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) + ) + { + continue; + } + + sharpCompressStream.Position = pos; + if (wrapper.CompressionType == CompressionType.LZip) + { + sharpCompressStream = await TarFactory + .BufferLZipHeaderAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false); + pos = sharpCompressStream.Position; + } + bool isTar; + { +#if STREAM_DISPOSEASYNC + await using var testStream = await CreateProbeDecompressionStreamAsync( +#else + using var testStream = await CreateProbeDecompressionStreamAsync( +#endif + sharpCompressStream, + wrapper.CompressionType, + readerOptions.Providers, + readerOptions, + cancellationToken + ) + .ConfigureAwait(false); + isTar = await TarArchive + .IsTarFileAsync(testStream, cancellationToken) + .ConfigureAwait(false); + } + if (isTar) + { + sharpCompressStream.Position = pos; + var reader = new TarReader( + sharpCompressStream, + readerOptions, + wrapper.CompressionType + ); + transferred = true; + return reader; + } + + if (wrapper.CompressionType != CompressionType.None) + { + throw new InvalidFormatException("Not a tar file."); + } } - if (wrapper.CompressionType != CompressionType.None) + sharpCompressStream.Position = pos; + var fallbackReader = new TarReader( + sharpCompressStream, + readerOptions, + CompressionType.None + ); + transferred = true; + return fallbackReader; + } + finally + { + // The reader owns the probe wrappers only after a successful open. + if (!transferred) { - throw new InvalidFormatException("Not a tar file."); +#if STREAM_DISPOSEASYNC + await sharpCompressStream.DisposeAsync().ConfigureAwait(false); +#else + sharpCompressStream.Dispose(); +#endif } } - - sharpCompressStream.Position = pos; - return new TarReader(sharpCompressStream, readerOptions, CompressionType.None); } public static async ValueTask OpenAsyncReader( @@ -210,7 +242,10 @@ public partial class TarReader TarWrapper.MaximumRewindBufferSize ) ); - sharpCompressStream.StartRecording(); + if (!sharpCompressStream.IsRecording) + { + sharpCompressStream.StartRecording(); + } long pos = sharpCompressStream.Position; foreach (var wrapper in TarWrapper.Wrappers) { diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index 51a6cf82..3fc1699a 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -68,13 +68,13 @@ public partial class TarReader : AbstractReader } finally { -#if LEGACY_DOTNET && !NETSTANDARD2_1 - ownedSource?.Dispose(); -#else +#if STREAM_DISPOSEASYNC if (ownedSource is not null) { await ownedSource.DisposeAsync().ConfigureAwait(false); } +#else + ownedSource?.Dispose(); #endif } } diff --git a/tests/SharpCompress.Test/Streams/TarLifetimeTests.cs b/tests/SharpCompress.Test/Streams/TarLifetimeTests.cs index 4f5a4f0a..d99d8555 100644 --- a/tests/SharpCompress.Test/Streams/TarLifetimeTests.cs +++ b/tests/SharpCompress.Test/Streams/TarLifetimeTests.cs @@ -6,8 +6,10 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Compressors.Deflate; +using SharpCompress.Compressors.LZMA; using SharpCompress.Factories; using SharpCompress.IO; +using SharpCompress.Providers; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Test.Mocks; @@ -18,6 +20,117 @@ namespace SharpCompress.Test.Streams; public class TarLifetimeTests : TestBase { + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Reader_Preserves_Existing_Recording_Session(bool async) + { + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")); + using var input = new ForwardOnlyStream(file); + using var buffered = SharpCompressStream.Create(input, TarWrapper.MaximumRewindBufferSize); + buffered.StartRecording(); + var options = new ReaderOptions { LeaveStreamOpen = false }; + if (async) + { + await using var reader = await TarReader.OpenAsyncReader( + buffered, + options, + CancellationToken.None + ); + Assert.True(await reader.MoveToNextEntryAsync(CancellationToken.None)); + } + else + { + using var reader = TarReader.OpenReader(buffered, options); + Assert.True(reader.MoveToNextEntry()); + } + } + + [Theory] + [InlineData("invalid", false)] + [InlineData("invalid", true)] + [InlineData("throw", false)] + [InlineData("throw", true)] + [InlineData("cancel", false)] + [InlineData("cancel", true)] + public async Task Failed_LZip_Probe_Releases_Buffer(string failure, bool leaveOpen) + { + using var source = new MemoryStream(); + var content = new byte[1024]; + new Random(42).NextBytes(content); + using ( + var lzip = LZipStream.Create( + SharpCompressStream.CreateNonDisposing(source), + SharpCompress.Compressors.CompressionMode.Compress + ) + ) + { + lzip.Write(content, 0, content.Length); + } + source.Position = 0; + using var cancellation = new CancellationTokenSource(); + var rings = new List(); + var provider = new ProbeLZipProvider(stream => + { + rings.AddRange(GetRings(stream)); + if (failure == "throw") + { + throw new IOException("Probe creation failed."); + } + if (failure == "cancel") + { + cancellation.Cancel(); + cancellation.Token.ThrowIfCancellationRequested(); + } + return LZipStream.Create(stream, SharpCompress.Compressors.CompressionMode.Decompress); + }); + var options = new ReaderOptions + { + LeaveStreamOpen = leaveOpen, + Providers = CompressionProviderRegistry.Default.With(provider), + }; + var exception = await Record.ExceptionAsync(async () => + { + await using var reader = await TarReader.OpenAsyncReader( + source, + options, + cancellation.Token + ); + }); + if (failure == "invalid") + { + Assert.IsType(exception); + } + else if (failure == "throw") + { + Assert.IsType(exception); + } + else + { + Assert.IsAssignableFrom(exception); + } + Assert.NotEmpty(rings); + var buffer = typeof(RingBuffer).GetField( + "_buffer", + BindingFlags.Instance | BindingFlags.NonPublic + )!; + Assert.All(rings, ring => Assert.Null(buffer.GetValue(ring))); + Assert.Equal(leaveOpen, source.CanRead); + } + + private sealed class ProbeLZipProvider(Func createStream) + : CompressionProviderBase + { + public override CompressionType CompressionType => CompressionType.LZip; + public override bool SupportsCompression => false; + public override bool SupportsDecompression => true; + + public override Stream CreateCompressStream(Stream destination, int compressionLevel) => + throw new NotSupportedException(); + + public override Stream CreateDecompressStream(Stream source) => createStream(source); + } + public static IEnumerable ReaderCases() { foreach (