Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
89420d43cf Fix compressed TAR formats broken in ArchiveFactory
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-11 17:58:25 +00:00
copilot-swe-agent[bot]
0696bf5efc Complete fix for compressed TAR formats issue
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-11 17:55:10 +00:00
copilot-swe-agent[bot]
3ad39f96da Remove redundant Rewind() call after StartRecording()
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-11 17:52:28 +00:00
copilot-swe-agent[bot]
649729d520 Fix compressed TAR formats in ArchiveFactory
- Detect compressed TAR formats (gz, bz2, xz, lz, zst, Z) in TarFactory.OpenArchive
- Decompress to MemoryStream for Archive API seekability requirement
- Handle async-only streams by skipping format detection
- Add tests for all compressed TAR formats with ArchiveFactory

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-11 17:50:16 +00:00
copilot-swe-agent[bot]
31ed7b822e Initial plan 2026-02-11 17:40:01 +00:00
Adam Hathcock
8a54f253d5 Merge pull request #1200 from adamhathcock/adam/fix-async-7z-seeking 2026-02-11 12:35:18 +00:00
Adam Hathcock
d0baa16502 Fix 7z seeking to be contigous in async too 2026-02-11 12:16:19 +00:00
5 changed files with 276 additions and 15 deletions

View File

@@ -182,15 +182,15 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
);
}
// Wrap with SyncOnlyStream to work around LZMA async bugs
// Return a ReadOnlySubStream that reads from the shared folder stream
return CreateEntryStream(
new SyncOnlyStream(
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
)
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
);
}
protected override ValueTask<EntryStream> GetEntryStreamAsync(
CancellationToken cancellationToken = default
) => new(GetEntryStream());
public override void Dispose()
{
_currentFolderStream?.Dispose();

View File

@@ -112,16 +112,149 @@ public class TarFactory
#region IArchiveFactory
/// <inheritdoc/>
public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
TarArchive.OpenArchive(stream, readerOptions);
public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
readerOptions ??= new ReaderOptions();
// Try to detect compressed TAR formats
// For async-only streams, skip detection and assume uncompressed
bool canDoSyncDetection = true;
try
{
// Test if we can do synchronous reads
var testBuffer = new byte[1];
var pos = stream.Position;
stream.Read(testBuffer, 0, 0); // Try a zero-length read
stream.Position = pos;
}
catch (NotSupportedException)
{
// Stream doesn't support synchronous reads
canDoSyncDetection = false;
}
if (!canDoSyncDetection)
{
// For async-only streams, we can't do format detection
// Assume it's an uncompressed TAR
return TarArchive.OpenArchive(stream, readerOptions);
}
var sharpCompressStream = new SharpCompressStream(stream);
sharpCompressStream.StartRecording();
foreach (var wrapper in TarWrapper.Wrappers)
{
sharpCompressStream.Rewind();
if (wrapper.IsMatch(sharpCompressStream))
{
sharpCompressStream.Rewind();
var decompressedStream = wrapper.CreateStream(sharpCompressStream);
if (TarArchive.IsTarFile(decompressedStream))
{
sharpCompressStream.StopRecording();
// For compressed TAR files, we need to decompress to a seekable stream
// since Archive API requires seekable streams
if (wrapper.CompressionType != CompressionType.None)
{
// Rewind and create a fresh decompression stream
sharpCompressStream.Rewind();
decompressedStream = wrapper.CreateStream(sharpCompressStream);
// Decompress to a MemoryStream to make it seekable
var memoryStream = new MemoryStream();
decompressedStream.CopyTo(memoryStream);
memoryStream.Position = 0;
// If we shouldn't leave the stream open, close the original
if (!readerOptions.LeaveStreamOpen)
{
stream.Dispose();
}
// Open the decompressed TAR with LeaveStreamOpen = false
// so the MemoryStream gets cleaned up with the archive
return TarArchive.OpenArchive(
memoryStream,
readerOptions with
{
LeaveStreamOpen = false,
}
);
}
// For uncompressed TAR, use the original stream directly
sharpCompressStream.Rewind();
return TarArchive.OpenArchive(stream, readerOptions);
}
}
}
// Fallback: try opening as uncompressed TAR
sharpCompressStream.StopRecording();
return TarArchive.OpenArchive(stream, readerOptions);
}
/// <inheritdoc/>
public IAsyncArchive OpenAsyncArchive(Stream stream, ReaderOptions? readerOptions = null) =>
(IAsyncArchive)OpenArchive(stream, readerOptions);
/// <inheritdoc/>
public IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
TarArchive.OpenArchive(fileInfo, readerOptions);
public IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= new ReaderOptions();
// Open the file and check if it's compressed
using var testStream = fileInfo.OpenRead();
var sharpCompressStream = new SharpCompressStream(testStream);
sharpCompressStream.StartRecording();
foreach (var wrapper in TarWrapper.Wrappers)
{
sharpCompressStream.Rewind();
if (wrapper.IsMatch(sharpCompressStream))
{
sharpCompressStream.Rewind();
var decompressedStream = wrapper.CreateStream(sharpCompressStream);
if (TarArchive.IsTarFile(decompressedStream))
{
sharpCompressStream.StopRecording();
// For compressed TAR files, decompress to memory
if (wrapper.CompressionType != CompressionType.None)
{
// Reopen file and decompress
using var fileStream = fileInfo.OpenRead();
var compressedStream = new SharpCompressStream(fileStream);
compressedStream.StartRecording();
var decompStream = wrapper.CreateStream(compressedStream);
var memoryStream = new MemoryStream();
decompStream.CopyTo(memoryStream);
memoryStream.Position = 0;
// Open with LeaveStreamOpen = false so MemoryStream gets cleaned up
return TarArchive.OpenArchive(
memoryStream,
readerOptions with
{
LeaveStreamOpen = false,
}
);
}
// Uncompressed, can use TarArchive's FileInfo overload directly
break;
}
}
}
// Open as regular TAR file
return TarArchive.OpenArchive(fileInfo, readerOptions);
}
/// <inheritdoc/>
public IAsyncArchive OpenAsyncArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>

View File

@@ -216,9 +216,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.0, )",
"resolved": "10.0.0",
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
"requested": "[10.0.2, )",
"resolved": "10.0.2",
"contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -264,9 +264,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.22, )",
"resolved": "8.0.22",
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
"requested": "[8.0.23, )",
"resolved": "8.0.23",
"contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",

View File

@@ -3,6 +3,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using Xunit;
@@ -224,4 +226,95 @@ public class SevenZipArchiveAsyncTests : ArchiveTests
VerifyFiles();
}
[Fact]
public async Task SevenZipArchive_Solid_ExtractAllEntries_Contiguous_Async()
{
// This test verifies that solid archives iterate entries as contiguous streams
// rather than recreating the decompression stream for each entry
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z");
await using var archive = SevenZipArchive.OpenAsyncArchive(testArchive);
Assert.True(((SevenZipArchive)archive).IsSolid);
await using var reader = await archive.ExtractAllEntriesAsync();
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
}
}
VerifyFiles();
}
[Fact]
public async Task SevenZipArchive_Solid_VerifyStreamReuse()
{
// This test verifies that the folder stream is reused within each folder
// and not recreated for each entry in solid archives
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z");
await using var archive = SevenZipArchive.OpenAsyncArchive(testArchive);
Assert.True(((SevenZipArchive)archive).IsSolid);
await using var reader = await archive.ExtractAllEntriesAsync();
var sevenZipReader = Assert.IsType<SevenZipArchive.SevenZipReader>(reader);
sevenZipReader.DiagnosticsEnabled = true;
Stream? currentFolderStreamInstance = null;
object? currentFolder = null;
var entryCount = 0;
var entriesInCurrentFolder = 0;
var streamRecreationsWithinFolder = 0;
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
// Extract the entry to trigger GetEntryStream
using var entryStream = await reader.OpenEntryStreamAsync();
var buffer = new byte[4096];
while (entryStream.Read(buffer, 0, buffer.Length) > 0)
{
// Read the stream to completion
}
entryCount++;
var folderStream = sevenZipReader.DiagnosticsCurrentFolderStream;
var folder = sevenZipReader.DiagnosticsCurrentFolder;
Assert.NotNull(folderStream); // Folder stream should exist
// Check if we're in a new folder
if (currentFolder == null || !ReferenceEquals(currentFolder, folder))
{
// Starting a new folder
currentFolder = folder;
currentFolderStreamInstance = folderStream;
entriesInCurrentFolder = 1;
}
else
{
// Same folder - verify stream wasn't recreated
entriesInCurrentFolder++;
if (!ReferenceEquals(currentFolderStreamInstance, folderStream))
{
// Stream was recreated within the same folder - this is the bug we're testing for!
streamRecreationsWithinFolder++;
}
currentFolderStreamInstance = folderStream;
}
}
}
// Verify we actually tested multiple entries
Assert.True(entryCount > 1, "Test should have multiple entries to verify stream reuse");
// The critical check: within a single folder, the stream should NEVER be recreated
Assert.Equal(0, streamRecreationsWithinFolder); // Folder stream should remain the same for all entries in the same folder
}
}

View File

@@ -308,4 +308,39 @@ public class TarArchiveTests : ArchiveTests
Assert.False(isTar);
}
[Theory]
[InlineData("Tar.tar.gz")]
[InlineData("Tar.tar.bz2")]
[InlineData("Tar.tar.xz")]
[InlineData("Tar.tar.lz")]
[InlineData("Tar.tar.zst")]
[InlineData("Tar.tar.Z")]
[InlineData("Tar.oldgnu.tar.gz")]
[InlineData("TarWithSymlink.tar.gz")]
public void Tar_Compressed_Archive_Factory(string filename)
{
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, filename);
using Stream stream = File.OpenRead(archiveFullPath);
using var archive = ArchiveFactory.OpenArchive(stream);
Assert.True(archive.Type == ArchiveType.Tar);
Assert.True(archive.Entries.Any());
}
[Theory]
[InlineData("Tar.tar.gz")]
[InlineData("Tar.tar.bz2")]
[InlineData("Tar.tar.xz")]
[InlineData("Tar.tar.lz")]
[InlineData("Tar.tar.zst")]
[InlineData("Tar.tar.Z")]
[InlineData("Tar.oldgnu.tar.gz")]
[InlineData("TarWithSymlink.tar.gz")]
public void Tar_Compressed_Archive_Factory_FromFile(string filename)
{
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, filename);
using var archive = ArchiveFactory.OpenArchive(archiveFullPath);
Assert.True(archive.Type == ArchiveType.Tar);
Assert.True(archive.Entries.Any());
}
}