diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index ac584e1b..a8cb18e2 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -119,6 +119,26 @@ public partial class TarArchive { if (header != null) { + // For compressed (streaming) archives, buffer each entry's packed stream into + // memory immediately. This ensures the underlying compressed stream advances + // past each entry's data, and allows entry streams to be opened after the + // full entry list has been enumerated. + if (_compressionType != CompressionType.None && header.PackedStream != null) + { + var buffered = new MemoryStream(); +#if NET8_0_OR_GREATER + await using (header.PackedStream) +#else + using (header.PackedStream) +#endif + { + await header.PackedStream.CopyToAsync(buffered).ConfigureAwait(false); + // Disposing TarReadOnlySubStream skips any remaining bytes and padding + } + buffered.Position = 0; + header.PackedStream = buffered; + } + if (header.EntryType == EntryType.LongName) { previousHeader = header; @@ -137,22 +157,37 @@ public partial class TarArchive ReaderOptions ); - var oldStreamPos = stream.Position; - - using (var entryStream = entry.OpenEntryStream()) + if (stream.CanSeek) { + var oldStreamPos = stream.Position; + + using (var entryStream = entry.OpenEntryStream()) + { + using var memoryStream = new MemoryStream(); + await entryStream + .CopyToAsync(memoryStream) + .ConfigureAwait(false); + var bytes = memoryStream.ToArray(); + + header.Name = ReaderOptions + .ArchiveEncoding.Decode(bytes) + .TrimNulls(); + } + + stream.Position = oldStreamPos; + } + else + { + // Streaming mode: PackedStream is already a buffered MemoryStream + using var entryStream = entry.OpenEntryStream(); using var memoryStream = new MemoryStream(); await entryStream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; var bytes = memoryStream.ToArray(); - header.Name = ReaderOptions .ArchiveEncoding.Decode(bytes) .TrimNulls(); } - stream.Position = oldStreamPos; - previousHeader = null; } yield return new TarArchiveEntry( diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index fdd8ac3a..3a3b73ba 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -129,6 +129,22 @@ public partial class TarArchive { if (header != null) { + // For compressed (streaming) archives, buffer each entry's packed stream into + // memory immediately. This ensures the underlying compressed stream advances + // past each entry's data, and allows entry streams to be opened after the + // full entry list has been enumerated. + if (_compressionType != CompressionType.None && header.PackedStream != null) + { + var buffered = new MemoryStream(); + using (header.PackedStream) + { + header.PackedStream.CopyTo(buffered, Constants.BufferSize); + // Disposing TarReadOnlySubStream skips any remaining bytes and padding + } + buffered.Position = 0; + header.PackedStream = buffered; + } + if (header.EntryType == EntryType.LongName) { previousHeader = header; @@ -147,20 +163,33 @@ public partial class TarArchive ReaderOptions ); - var oldStreamPos = stream.Position; - - using (var entryStream = entry.OpenEntryStream()) + if (stream.CanSeek) { + var oldStreamPos = stream.Position; + + using (var entryStream = entry.OpenEntryStream()) + { + using var memoryStream = new MemoryStream(); + entryStream.CopyTo(memoryStream, Constants.BufferSize); + var bytes = memoryStream.ToArray(); + + header.Name = ReaderOptions + .ArchiveEncoding.Decode(bytes) + .TrimNulls(); + } + + stream.Position = oldStreamPos; + } + else + { + // Streaming mode: PackedStream is already a buffered MemoryStream + using var entryStream = entry.OpenEntryStream(); using var memoryStream = new MemoryStream(); entryStream.CopyTo(memoryStream, Constants.BufferSize); - memoryStream.Position = 0; var bytes = memoryStream.ToArray(); - header.Name = ReaderOptions.ArchiveEncoding.Decode(bytes).TrimNulls(); } - stream.Position = oldStreamPos; - previousHeader = null; } yield return new TarArchiveEntry( diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index b44de28b..5059aafe 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.2, )", - "resolved": "10.0.2", - "contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.23, )", - "resolved": "8.0.23", - "contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index b9b7d6eb..c34dcab5 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -275,4 +275,50 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.Equal(2, numberOfEntries); } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + public async ValueTask CompressedTar_AsyncArchive_Lists_All_Entries_Async( + string archiveFileName + ) + { + await using var archive = await TarArchive.OpenAsyncArchive( + new AsyncOnlyStream(File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveFileName))), + new ReaderOptions { LeaveStreamOpen = false } + ); + + Assert.Equal(6, await archive.EntriesAsync.CountAsync()); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + public async ValueTask CompressedTar_AsyncArchive_Entry_Streams_Are_Readable_Async( + string archiveFileName + ) + { + await using var archive = await TarArchive.OpenAsyncArchive( + new AsyncOnlyStream(File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveFileName))), + new ReaderOptions { LeaveStreamOpen = false } + ); + + await foreach ( + var entry in archive.EntriesAsync.Where(e => !e.IsDirectory).ConfigureAwait(false) + ) + { +#if LEGACY_DOTNET + using var entryStream = await entry.OpenEntryStreamAsync(); +#else + await using var entryStream = await entry.OpenEntryStreamAsync(); +#endif + using var ms = new MemoryStream(); + await entryStream.CopyToAsync(ms); + Assert.Equal(entry.Size, ms.Length); + } + } } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 086ca02c..84d1b9cb 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -339,4 +339,37 @@ public class TarArchiveTests : ArchiveTests Assert.Equal(ArchiveType.Tar, reader.Type); Assert.True(reader.MoveToNextEntry()); } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + public void CompressedTar_Archive_Lists_All_Entries(string archiveFileName) + { + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveFileName)); + using var archive = ArchiveFactory.OpenArchive(stream); + + Assert.Equal(ArchiveType.Tar, archive.Type); + Assert.Equal(6, archive.Entries.Count()); + } + + [Theory] + [InlineData("Tar.tar.gz")] + [InlineData("Tar.tar.bz2")] + [InlineData("Tar.tar.xz")] + [InlineData("Tar.tar.zst")] + public void CompressedTar_Archive_Entry_Streams_Are_Readable(string archiveFileName) + { + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveFileName)); + using var archive = ArchiveFactory.OpenArchive(stream); + + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + using var ms = new MemoryStream(); + entryStream.CopyTo(ms); + Assert.Equal(entry.Size, ms.Length); + } + } }