mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-23 23:45:14 +00:00
Fix compressed tar archive entry enumeration via ArchiveFactory.OpenArchive
When reading compressed tar files (gzip, bzip2, xz, zstd, lzw, lzip) via ArchiveFactory.OpenArchive, entry enumeration would fail with IncompleteArchiveException after the second non-zero-sized entry. Root cause: TarHeaderFactory.ReadHeader (streaming mode) created a TarReadOnlySubStream for each entry's data but never disposed it before reading the next 512-byte header. This left the underlying decompressed stream positioned at the previous entry's data, causing header reads to read garbage. Fix: 1. TarHeaderFactory.ReadHeader / ReadHeaderAsync: track the previous TarReadOnlySubStream and dispose it (skipping any unread data + padding) before reading the next header. Double-dispose is safe because TarReadOnlySubStream guards against it with _isDisposed. 2. TarArchive.LoadEntries / LoadEntriesAsync: handle LongName entries differently for streaming (compressed) vs seekable (uncompressed) mode. In streaming mode the long filename is read immediately from header.PackedStream when the LongName header is received, before the foreach advances and ReadHeader disposes the packed stream. In seekable mode the existing behavior is preserved. Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/8feadf56-d10d-431f-834d-bf1237a577ec Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
21d2c406f3
commit
0fe4b6bf21
@@ -109,6 +109,7 @@ public partial class TarArchive
|
||||
{
|
||||
// Use async header reading for async-only streams
|
||||
TarHeader? previousHeader = null;
|
||||
string? previousLongName = null;
|
||||
await foreach (
|
||||
var header in TarHeaderFactory.ReadHeaderAsync(
|
||||
streamingMode,
|
||||
@@ -121,18 +122,48 @@ public partial class TarArchive
|
||||
{
|
||||
if (header.EntryType == EntryType.LongName)
|
||||
{
|
||||
previousHeader = header;
|
||||
if (_compressionType != CompressionType.None)
|
||||
{
|
||||
// Streaming (compressed) mode: read the long name immediately
|
||||
// from the PackedStream before the foreach loop advances and
|
||||
// ReadHeaderAsync disposes it while skipping to the next header.
|
||||
var longNameStream = header.PackedStream.NotNull();
|
||||
#if NET8_0_OR_GREATER
|
||||
await using (longNameStream.ConfigureAwait(false))
|
||||
#else
|
||||
using (longNameStream)
|
||||
#endif
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
await longNameStream
|
||||
.CopyToAsync(memoryStream)
|
||||
.ConfigureAwait(false);
|
||||
previousLongName = ReaderOptions
|
||||
.ArchiveEncoding.Decode(memoryStream.ToArray())
|
||||
.TrimNulls();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Seekable (uncompressed) mode: save the header; the long name
|
||||
// will be read after the next entry header has been parsed.
|
||||
previousHeader = header;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousHeader != null)
|
||||
if (previousLongName != null)
|
||||
{
|
||||
// Apply the long name that was pre-read in streaming mode.
|
||||
header.Name = previousLongName;
|
||||
previousLongName = null;
|
||||
}
|
||||
else if (previousHeader != null)
|
||||
{
|
||||
// Seekable mode: read the long name via the seekable stream now.
|
||||
var entry = new TarArchiveEntry(
|
||||
this,
|
||||
new TarFilePart(
|
||||
previousHeader,
|
||||
_compressionType == CompressionType.None ? stream : null
|
||||
),
|
||||
new TarFilePart(previousHeader, stream),
|
||||
CompressionType.None,
|
||||
ReaderOptions
|
||||
);
|
||||
|
||||
@@ -117,6 +117,7 @@ public partial class TarArchive
|
||||
stream.Position = 0;
|
||||
}
|
||||
TarHeader? previousHeader = null;
|
||||
string? previousLongName = null;
|
||||
foreach (
|
||||
var header in TarHeaderFactory.ReadHeader(
|
||||
_compressionType == CompressionType.None
|
||||
@@ -131,18 +132,39 @@ public partial class TarArchive
|
||||
{
|
||||
if (header.EntryType == EntryType.LongName)
|
||||
{
|
||||
previousHeader = header;
|
||||
if (_compressionType != CompressionType.None)
|
||||
{
|
||||
// Streaming (compressed) mode: read the long name immediately
|
||||
// from the PackedStream before the foreach loop advances and
|
||||
// ReadHeader disposes it while skipping to the next header.
|
||||
using var longNameStream = header.PackedStream.NotNull();
|
||||
using var memoryStream = new MemoryStream();
|
||||
longNameStream.CopyTo(memoryStream, Constants.BufferSize);
|
||||
previousLongName = ReaderOptions
|
||||
.ArchiveEncoding.Decode(memoryStream.ToArray())
|
||||
.TrimNulls();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Seekable (uncompressed) mode: save the header; the long name
|
||||
// will be read after the next entry header has been parsed.
|
||||
previousHeader = header;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousHeader != null)
|
||||
if (previousLongName != null)
|
||||
{
|
||||
// Apply the long name that was pre-read in streaming mode.
|
||||
header.Name = previousLongName;
|
||||
previousLongName = null;
|
||||
}
|
||||
else if (previousHeader != null)
|
||||
{
|
||||
// Seekable mode: read the long name via the seekable stream now.
|
||||
var entry = new TarArchiveEntry(
|
||||
this,
|
||||
new TarFilePart(
|
||||
previousHeader,
|
||||
_compressionType == CompressionType.None ? stream : null
|
||||
),
|
||||
new TarFilePart(previousHeader, stream),
|
||||
CompressionType.None,
|
||||
ReaderOptions
|
||||
);
|
||||
|
||||
@@ -19,11 +19,26 @@ internal static partial class TarHeaderFactory
|
||||
using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
|
||||
#endif
|
||||
|
||||
// In streaming mode we track the previous entry's packed stream so we can
|
||||
// advance past its data (and alignment padding) before reading the next header.
|
||||
TarReadOnlySubStream? previousPackedStream = null;
|
||||
while (true)
|
||||
{
|
||||
TarHeader? header = null;
|
||||
try
|
||||
{
|
||||
// Dispose the previous packed stream so any unread entry data and its
|
||||
// 512-byte alignment padding are skipped before we read the next header.
|
||||
if (previousPackedStream != null)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
previousPackedStream.Dispose();
|
||||
#else
|
||||
await previousPackedStream.DisposeAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
previousPackedStream = null;
|
||||
}
|
||||
|
||||
header = new TarHeader(archiveEncoding);
|
||||
if (!await header.ReadAsync(reader).ConfigureAwait(false))
|
||||
{
|
||||
@@ -45,11 +60,13 @@ internal static partial class TarHeaderFactory
|
||||
#if LEGACY_DOTNET
|
||||
useSyncOverAsync = true;
|
||||
#endif
|
||||
header.PackedStream = new TarReadOnlySubStream(
|
||||
var packedStream = new TarReadOnlySubStream(
|
||||
stream,
|
||||
header.Size,
|
||||
useSyncOverAsync
|
||||
);
|
||||
header.PackedStream = packedStream;
|
||||
previousPackedStream = packedStream;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -13,11 +13,20 @@ internal static partial class TarHeaderFactory
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
// In streaming mode we track the previous entry's packed stream so we can
|
||||
// advance past its data (and alignment padding) before reading the next header.
|
||||
// Without this, the loop would re-read entry data bytes as a header.
|
||||
TarReadOnlySubStream? previousPackedStream = null;
|
||||
while (true)
|
||||
{
|
||||
TarHeader? header = null;
|
||||
try
|
||||
{
|
||||
// Dispose the previous packed stream so any unread entry data and its
|
||||
// 512-byte alignment padding are skipped before we read the next header.
|
||||
previousPackedStream?.Dispose();
|
||||
previousPackedStream = null;
|
||||
|
||||
var reader = new BinaryReader(stream, archiveEncoding.Default, leaveOpen: false);
|
||||
header = new TarHeader(archiveEncoding);
|
||||
|
||||
@@ -37,11 +46,9 @@ internal static partial class TarHeaderFactory
|
||||
break;
|
||||
case StreamingMode.Streaming:
|
||||
{
|
||||
header.PackedStream = new TarReadOnlySubStream(
|
||||
stream,
|
||||
header.Size,
|
||||
false
|
||||
);
|
||||
var packedStream = new TarReadOnlySubStream(stream, header.Size, false);
|
||||
header.PackedStream = packedStream;
|
||||
previousPackedStream = packedStream;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -275,4 +275,23 @@ 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 TarArchive_CompressedTar_AllEntriesCanBeEnumerated_Async(
|
||||
string archiveFileName
|
||||
)
|
||||
{
|
||||
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveFileName));
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(stream);
|
||||
|
||||
Assert.Equal(ArchiveType.Tar, archive.Type);
|
||||
// The compressed archives contain 6 entries (directories + files).
|
||||
// This test verifies that entry enumeration does not fail after the first
|
||||
// non-empty entry, which was broken for compressed (streaming) tars.
|
||||
Assert.Equal(6, await archive.EntriesAsync.CountAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +330,49 @@ public class TarArchiveTests : ArchiveTests
|
||||
Assert.NotEmpty(archive.Entries);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Tar.tar.gz")]
|
||||
[InlineData("Tar.tar.bz2")]
|
||||
[InlineData("Tar.tar.xz")]
|
||||
[InlineData("Tar.tar.zst")]
|
||||
public void TarArchive_CompressedTar_AllEntriesCanBeEnumerated(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);
|
||||
// The compressed archives contain 6 entries (directories + files).
|
||||
// This test verifies that entry enumeration does not fail after the first
|
||||
// non-empty entry, which was broken for compressed (streaming) tars.
|
||||
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 TarArchive_CompressedTar_EntryContentsCanBeReadDuringIteration(
|
||||
string archiveFileName
|
||||
)
|
||||
{
|
||||
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveFileName));
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
|
||||
// Compressed (streaming) tars must be read in order: open and read each
|
||||
// entry's content before advancing to the next entry.
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var buffer = new MemoryStream();
|
||||
entryStream.CopyTo(buffer);
|
||||
Assert.Equal(entry.Size, buffer.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarReaderStreamRead_Autodetect_CompressedTar()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user