Fix TAR probe cleanup and preserve active recording sessions

This commit is contained in:
Adam Hathcock
2026-10-16 15:08:36 +01:00
parent c593d00651
commit 9bb74557d5
4 changed files with 207 additions and 59 deletions

View File

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

View File

@@ -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<IAsyncReader> 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)
{

View File

@@ -68,13 +68,13 @@ public partial class TarReader : AbstractReader<TarEntry, TarVolume>
}
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
}
}

View File

@@ -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<RingBuffer>();
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<InvalidFormatException>(exception);
}
else if (failure == "throw")
{
Assert.IsType<IOException>(exception);
}
else
{
Assert.IsAssignableFrom<OperationCanceledException>(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<Stream, Stream> 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<object[]> ReaderCases()
{
foreach (