mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-12 13:35:11 +00:00
Compare commits
7 Commits
adam/fix-e
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89420d43cf | ||
|
|
0696bf5efc | ||
|
|
3ad39f96da | ||
|
|
649729d520 | ||
|
|
31ed7b822e | ||
|
|
8a54f253d5 | ||
|
|
d0baa16502 |
@@ -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();
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user