Fix compressed Tar files not listing all entries in archive API

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/c7d10bd8-f355-4ff4-bd72-d7a9e4e02e92
This commit is contained in:
copilot-swe-agent[bot]
2026-03-20 17:31:34 +00:00
parent 0241deebe5
commit 6db7bbf52f
5 changed files with 163 additions and 20 deletions

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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