Merge branch 'master' into adam/fix-editorconfig-errors
This commit is contained in:
commit
2a613a8427
10 changed files with 145 additions and 9 deletions
25
docs/API.md
25
docs/API.md
|
|
@ -485,6 +485,31 @@ using (var archive = ZipArchive.CreateArchive())
|
|||
}
|
||||
```
|
||||
|
||||
### Buffered Forward-Only Streams
|
||||
|
||||
`SharpCompressStream` can wrap streams with buffering for forward-only scenarios:
|
||||
|
||||
```csharp
|
||||
// Wrap a non-seekable stream with buffering
|
||||
using (var bufferedStream = new SharpCompressStream(rawStream))
|
||||
{
|
||||
// Provides ring buffer functionality for reading ahead
|
||||
// and seeking within buffered data
|
||||
using (var reader = ReaderFactory.OpenReader(bufferedStream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
reader.WriteEntryToDirectory(@"C:\output");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Useful for:
|
||||
- Non-seekable streams (network streams, pipes)
|
||||
- Forward-only reading with limited look-ahead
|
||||
- Buffering unbuffered streams for better performance
|
||||
|
||||
### Extract Specific Files
|
||||
|
||||
```csharp
|
||||
|
|
|
|||
|
|
@ -164,6 +164,12 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
|
|||
|
||||
var folder = entry.FilePart.Folder;
|
||||
|
||||
// If folder is null (empty stream entry), return empty stream
|
||||
if (folder is null)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
|
||||
// Check if we're starting a new folder - dispose old folder stream if needed
|
||||
if (folder != _currentFolder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
internal partial class SharpCompressStream
|
||||
public partial class SharpCompressStream
|
||||
{
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using SharpCompress.Common;
|
|||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
internal partial class SharpCompressStream
|
||||
public partial class SharpCompressStream
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a SharpCompressStream that acts as a passthrough wrapper.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,19 @@ using SharpCompress.Common;
|
|||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
internal partial class SharpCompressStream : Stream, IStreamStack
|
||||
/// <summary>
|
||||
/// Stream wrapper that provides optional ring-buffered reading for non-seekable
|
||||
/// or forward-only streams, enabling limited backward seeking required by some
|
||||
/// decompressors and archive formats.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In most cases, callers should obtain an instance via the static
|
||||
/// <c>SharpCompressStream.Create(...)</c> methods rather than constructing this
|
||||
/// class directly. The <c>Create</c> methods select an appropriate configuration
|
||||
/// (such as passthrough vs buffered mode and buffer size) for the underlying
|
||||
/// stream and usage scenario.
|
||||
/// </remarks>
|
||||
public partial class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
public virtual Stream BaseStream() => stream;
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
using Xunit;
|
||||
|
|
@ -344,4 +345,53 @@ public class SevenZipArchiveTests : ArchiveTests
|
|||
// 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
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_EmptyStream_WriteToDirectory()
|
||||
{
|
||||
// This test specifically verifies that archives with empty-stream entries
|
||||
// (files with size 0 and no compressed data) can be extracted without throwing
|
||||
// NullReferenceException. This was previously failing because the folder was null
|
||||
// for empty-stream entries.
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.EmptyStream.7z");
|
||||
using var archive = SevenZipArchive.OpenArchive(testArchive);
|
||||
|
||||
var emptyStreamFileCount = 0;
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
// Verify this is actually an empty-stream entry (HasStream == false)
|
||||
var sevenZipEntry = entry as SevenZipEntry;
|
||||
if (sevenZipEntry?.FilePart.Header.HasStream == false)
|
||||
{
|
||||
emptyStreamFileCount++;
|
||||
}
|
||||
|
||||
// This should not throw NullReferenceException
|
||||
entry.WriteToDirectory(SCRATCH_FILES_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we actually tested empty-stream entries
|
||||
Assert.True(
|
||||
emptyStreamFileCount > 0,
|
||||
"Test archive should contain at least one empty-stream entry"
|
||||
);
|
||||
|
||||
// Verify that empty files were created
|
||||
var extractedFiles = Directory.GetFiles(
|
||||
SCRATCH_FILES_PATH,
|
||||
"*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
Assert.NotEmpty(extractedFiles);
|
||||
|
||||
// All extracted files should be empty (0 bytes)
|
||||
foreach (var file in extractedFiles)
|
||||
{
|
||||
var fileInfo = new FileInfo(file);
|
||||
Assert.Equal(0, fileInfo.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -34,4 +35,24 @@ public class XzStreamAsyncTests : XzTestsBase
|
|||
var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false);
|
||||
Assert.Equal(OriginalIndexed, uncompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask CanReadNonSeekableStreamAsync()
|
||||
{
|
||||
var nonSeekable = new ForwardOnlyStream(new MemoryStream(Compressed));
|
||||
var xz = new XZStream(SharpCompressStream.Create(nonSeekable));
|
||||
using var sr = new StreamReader(new AsyncOnlyStream(xz));
|
||||
var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false);
|
||||
Assert.Equal(Original, uncompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask CanReadNonSeekableEmptyStreamAsync()
|
||||
{
|
||||
var nonSeekable = new ForwardOnlyStream(new MemoryStream(CompressedEmpty));
|
||||
var xz = new XZStream(SharpCompressStream.Create(nonSeekable));
|
||||
using var sr = new StreamReader(new AsyncOnlyStream(xz));
|
||||
var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false);
|
||||
Assert.Equal(OriginalEmpty, uncompressed);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using System.IO;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Xz;
|
||||
|
|
@ -32,4 +34,24 @@ public class XzStreamTests : XzTestsBase
|
|||
var uncompressed = sr.ReadToEnd();
|
||||
Assert.Equal(OriginalIndexed, uncompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadNonSeekableStream()
|
||||
{
|
||||
var nonSeekable = new ForwardOnlyStream(new MemoryStream(Compressed));
|
||||
var xz = new XZStream(SharpCompressStream.Create(nonSeekable));
|
||||
using var sr = new StreamReader(xz);
|
||||
var uncompressed = sr.ReadToEnd();
|
||||
Assert.Equal(Original, uncompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadNonSeekableEmptyStream()
|
||||
{
|
||||
var nonSeekable = new ForwardOnlyStream(new MemoryStream(CompressedEmpty));
|
||||
var xz = new XZStream(SharpCompressStream.Create(nonSeekable));
|
||||
using var sr = new StreamReader(xz);
|
||||
var uncompressed = sr.ReadToEnd();
|
||||
Assert.Equal(OriginalEmpty, uncompressed);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
tests/TestArchives/Archives/7Zip.EmptyStream.7z
Normal file
BIN
tests/TestArchives/Archives/7Zip.EmptyStream.7z
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue