mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-04 13:34:59 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d15f73f0 | ||
|
|
4e0d78d6c8 | ||
|
|
63a1927838 | ||
|
|
3d745bfa05 | ||
|
|
ce26d50792 | ||
|
|
01e162fcc4 | ||
|
|
443f7b8b0c | ||
|
|
df63e152c1 | ||
|
|
ad7e64ba43 | ||
|
|
8737b7a38e | ||
|
|
13199fcfd1 | ||
|
|
9a7bdd39e8 | ||
|
|
484bc740d7 | ||
|
|
8a67d501a8 | ||
|
|
3c87242bd0 | ||
|
|
999124e68e | ||
|
|
db2f5c9cb9 | ||
|
|
af08a7cd54 | ||
|
|
72eaf66f05 | ||
|
|
8a3be35d67 | ||
|
|
d59e4c2a0d | ||
|
|
71655e04c4 | ||
|
|
a706a9d725 | ||
|
|
970934a40b | ||
|
|
a9c28a7b62 | ||
|
|
4d31436740 | ||
|
|
c82744c51c | ||
|
|
f0eaddc6a6 | ||
|
|
d6156f0f1e | ||
|
|
3c88c7fdd5 | ||
|
|
d11f6aefb0 | ||
|
|
010a38bb73 | ||
|
|
53f12d75db | ||
|
|
6c866324b2 | ||
|
|
a114155189 | ||
|
|
014bbc3ea4 | ||
|
|
d52facd4ab | ||
|
|
0a50386ada | ||
|
|
b9fc680548 | ||
|
|
7dcc13c1f0 | ||
|
|
56d3091688 | ||
|
|
a0af0604d1 | ||
|
|
875c2d7694 | ||
|
|
8c95f863cb | ||
|
|
ddf37e82c2 | ||
|
|
a82fda98d7 | ||
|
|
44e4b1804e | ||
|
|
4ca1a7713e | ||
|
|
9caf7be928 | ||
|
|
bf4217fde6 | ||
|
|
d5a8c37113 | ||
|
|
21ce9a38e6 | ||
|
|
7732fbb698 |
@@ -13,8 +13,7 @@
|
||||
<PackageVersion Include="System.Memory" Version="4.6.3" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
|
||||
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102" />
|
||||
<GlobalPackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -230,7 +230,7 @@ static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not tagged - create prerelease version based on next minor version
|
||||
// Not tagged - create prerelease version
|
||||
var allTags = (await GetGitOutput("tag", "--list"))
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"))
|
||||
@@ -240,8 +240,22 @@ static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0";
|
||||
var lastVersion = Version.Parse(lastTag);
|
||||
|
||||
// Increment minor version for next release
|
||||
var nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
|
||||
// Determine version increment based on branch
|
||||
var currentBranch = await GetCurrentBranch();
|
||||
Version nextVersion;
|
||||
|
||||
if (currentBranch == "release")
|
||||
{
|
||||
// Release branch: increment patch version
|
||||
nextVersion = new Version(lastVersion.Major, lastVersion.Minor, lastVersion.Build + 1);
|
||||
Console.WriteLine($"Building prerelease for release branch (patch increment)");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Master or other branches: increment minor version
|
||||
nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
|
||||
Console.WriteLine($"Building prerelease for {currentBranch} branch (minor increment)");
|
||||
}
|
||||
|
||||
// Use commit count since the last version tag if available; otherwise, fall back to total count
|
||||
var revListArgs = allTags.Any() ? $"--count {lastTag}..HEAD" : "--count HEAD";
|
||||
@@ -253,6 +267,28 @@ static async Task<(string version, bool isPrerelease)> GetVersion()
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetCurrentBranch()
|
||||
{
|
||||
// In GitHub Actions, GITHUB_REF_NAME contains the branch name
|
||||
var githubRefName = Environment.GetEnvironmentVariable("GITHUB_REF_NAME");
|
||||
if (!string.IsNullOrEmpty(githubRefName))
|
||||
{
|
||||
return githubRefName;
|
||||
}
|
||||
|
||||
// Fallback to git command for local builds
|
||||
try
|
||||
{
|
||||
var (output, _) = await ReadAsync("git", "branch --show-current");
|
||||
return output.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Warning: Could not determine current branch: {ex.Message}");
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetGitOutput(string command, string args)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -14,11 +14,45 @@
|
||||
"resolved": "1.1.9",
|
||||
"contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"SimpleExec": {
|
||||
"type": "Direct",
|
||||
"requested": "[13.0.0, )",
|
||||
"resolved": "13.0.0",
|
||||
"contentHash": "zcCR1pupa1wI1VqBULRiQKeHKKZOuJhi/K+4V5oO+rHJZlaOD53ViFo1c3PavDoMAfSn/FAXGAWpPoF57rwhYg=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,22 +166,14 @@ public static class ArchiveFactory
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsArchive(
|
||||
string filePath,
|
||||
out ArchiveType? type,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsArchive(string filePath, out ArchiveType? type)
|
||||
{
|
||||
filePath.NotNullOrEmpty(nameof(filePath));
|
||||
using Stream s = File.OpenRead(filePath);
|
||||
return IsArchive(s, out type, bufferSize);
|
||||
return IsArchive(s, out type);
|
||||
}
|
||||
|
||||
public static bool IsArchive(
|
||||
Stream stream,
|
||||
out ArchiveType? type,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsArchive(Stream stream, out ArchiveType? type)
|
||||
{
|
||||
type = null;
|
||||
stream.NotNull(nameof(stream));
|
||||
|
||||
@@ -14,11 +14,8 @@ class AutoArchiveFactory : IArchiveFactory
|
||||
|
||||
public IEnumerable<string> GetSupportedExtensions() => throw new NotSupportedException();
|
||||
|
||||
public bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => throw new NotSupportedException();
|
||||
public bool IsArchive(Stream stream, string? password = null) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public FileInfo? GetFilePart(int index, FileInfo part1) => throw new NotSupportedException();
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static class IArchiveEntryExtensions
|
||||
{
|
||||
private const int BufferSize = 81920;
|
||||
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
{
|
||||
@@ -28,7 +26,7 @@ public static class IArchiveEntryExtensions
|
||||
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
sourceStream.CopyTo(streamToWriteTo, BufferSize);
|
||||
sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,7 +49,7 @@ public static class IArchiveEntryExtensions
|
||||
using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken);
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, BufferSize, cancellationToken)
|
||||
.CopyToAsync(streamToWriteTo, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +212,31 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
public override long TotalSize =>
|
||||
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
|
||||
|
||||
private sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
internal sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
|
||||
{
|
||||
private readonly SevenZipArchive _archive;
|
||||
private SevenZipEntry? _currentEntry;
|
||||
private Stream? _currentFolderStream;
|
||||
private CFolder? _currentFolder;
|
||||
|
||||
/// <summary>
|
||||
/// Enables internal diagnostics for tests.
|
||||
/// When disabled (default), diagnostics properties return null to avoid exposing internal state.
|
||||
/// </summary>
|
||||
internal bool DiagnosticsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current folder instance used to decide whether the solid folder stream should be reused.
|
||||
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
|
||||
/// </summary>
|
||||
internal object? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null;
|
||||
|
||||
/// <summary>
|
||||
/// Current shared folder stream instance.
|
||||
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
|
||||
/// </summary>
|
||||
internal Stream? DiagnosticsCurrentFolderStream =>
|
||||
DiagnosticsEnabled ? _currentFolderStream : null;
|
||||
|
||||
internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive)
|
||||
: base(readerOptions, ArchiveType.SevenZip) => this._archive = archive;
|
||||
@@ -231,9 +252,10 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
_currentEntry = dir;
|
||||
yield return dir;
|
||||
}
|
||||
// For non-directory entries, yield them without creating shared streams
|
||||
// Each call to GetEntryStream() will create a fresh decompression stream
|
||||
// to avoid state corruption issues with async operations
|
||||
// For solid archives (entries in the same folder share a compressed stream),
|
||||
// we must iterate entries sequentially and maintain the folder stream state
|
||||
// across entries in the same folder to avoid recreating the decompression
|
||||
// stream for each file, which breaks contiguous streaming.
|
||||
foreach (var entry in entries.Where(x => !x.IsDirectory))
|
||||
{
|
||||
_currentEntry = entry;
|
||||
@@ -243,19 +265,53 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
|
||||
protected override EntryStream GetEntryStream()
|
||||
{
|
||||
// Create a fresh decompression stream for each file (no state sharing).
|
||||
// However, the LZMA decoder has bugs in its async implementation that cause
|
||||
// state corruption even on fresh streams. The SyncOnlyStream wrapper
|
||||
// works around these bugs by forcing async operations to use sync equivalents.
|
||||
//
|
||||
// TODO: Fix the LZMA decoder async bugs (in LzmaStream, Decoder, OutWindow)
|
||||
// so this wrapper is no longer necessary.
|
||||
var entry = _currentEntry.NotNull("currentEntry is not null");
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
return CreateEntryStream(new SyncOnlyStream(entry.FilePart.GetCompressedStream()));
|
||||
|
||||
var filePart = (SevenZipFilePart)entry.FilePart;
|
||||
if (!filePart.Header.HasStream)
|
||||
{
|
||||
// Entries with no underlying stream (e.g., empty files or anti-items)
|
||||
// should return an empty stream, matching previous behavior.
|
||||
return CreateEntryStream(Stream.Null);
|
||||
}
|
||||
|
||||
var folder = filePart.Folder;
|
||||
// Check if we're starting a new folder - dispose old folder stream if needed
|
||||
if (folder != _currentFolder)
|
||||
{
|
||||
_currentFolderStream?.Dispose();
|
||||
_currentFolderStream = null;
|
||||
_currentFolder = folder;
|
||||
}
|
||||
|
||||
// Create the folder stream once per folder
|
||||
if (_currentFolderStream is null)
|
||||
{
|
||||
_currentFolderStream = _archive._database!.GetFolderStream(
|
||||
_archive.Volumes.Single().Stream,
|
||||
folder!,
|
||||
_archive._database.PasswordProvider
|
||||
);
|
||||
}
|
||||
|
||||
// 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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_currentFolderStream?.Dispose();
|
||||
_currentFolderStream = null;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
using (var entryStream = entry.OpenEntryStream())
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
entryStream.CopyTo(memoryStream);
|
||||
entryStream.CopyTo(memoryStream, Constants.BufferSize);
|
||||
memoryStream.Position = 0;
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
|
||||
@@ -124,38 +124,27 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
);
|
||||
}
|
||||
|
||||
public static bool IsZipFile(
|
||||
string filePath,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => IsZipFile(new FileInfo(filePath), password, bufferSize);
|
||||
public static bool IsZipFile(string filePath, string? password = null) =>
|
||||
IsZipFile(new FileInfo(filePath), password);
|
||||
|
||||
public static bool IsZipFile(
|
||||
FileInfo fileInfo,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsZipFile(FileInfo fileInfo, string? password = null)
|
||||
{
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using Stream stream = fileInfo.OpenRead();
|
||||
return IsZipFile(stream, password, bufferSize);
|
||||
return IsZipFile(stream, password);
|
||||
}
|
||||
|
||||
public static bool IsZipFile(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsZipFile(Stream stream, string? password = null)
|
||||
{
|
||||
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
|
||||
try
|
||||
{
|
||||
if (stream is not SharpCompressStream)
|
||||
{
|
||||
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
|
||||
stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize);
|
||||
}
|
||||
|
||||
var header = headerFactory
|
||||
@@ -177,18 +166,14 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsZipMulti(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public static bool IsZipMulti(Stream stream, string? password = null)
|
||||
{
|
||||
var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
|
||||
try
|
||||
{
|
||||
if (stream is not SharpCompressStream)
|
||||
{
|
||||
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
|
||||
stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize);
|
||||
}
|
||||
|
||||
var header = headerFactory
|
||||
@@ -229,7 +214,7 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
if (streams.Count() > 1) //test part 2 - true = multipart not split
|
||||
{
|
||||
streams[1].Position += 4; //skip the POST_DATA_DESCRIPTOR to prevent an exception
|
||||
var isZip = IsZipFile(streams[1], ReaderOptions.Password, ReaderOptions.BufferSize);
|
||||
var isZip = IsZipFile(streams[1], ReaderOptions.Password);
|
||||
streams[1].Position -= 4;
|
||||
if (isZip)
|
||||
{
|
||||
|
||||
10
src/SharpCompress/Common/Constants.cs
Normal file
10
src/SharpCompress/Common/Constants.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// The default buffer size for stream operations, matching .NET's Stream.CopyTo default of 81920 bytes.
|
||||
/// This can be modified globally at runtime.
|
||||
/// </summary>
|
||||
public static int BufferSize { get; set; } = 81920;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ internal class WinzipAesEncryptionData
|
||||
{
|
||||
_keySize = keySize;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
|
||||
|
||||
@@ -586,7 +586,13 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_stream.Flush();
|
||||
// Only flush the underlying stream when in write mode
|
||||
// Flushing input streams during read operations is not meaningful
|
||||
// and can cause issues with forward-only/non-seekable streams
|
||||
if (_streamMode == StreamMode.Writer)
|
||||
{
|
||||
_stream.Flush();
|
||||
}
|
||||
//rewind the buffer
|
||||
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
|
||||
z.AvailableBytesIn = 0;
|
||||
@@ -594,7 +600,13 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
// Only flush the underlying stream when in write mode
|
||||
// Flushing input streams during read operations is not meaningful
|
||||
// and can cause issues with forward-only/non-seekable streams
|
||||
if (_streamMode == StreamMode.Writer)
|
||||
{
|
||||
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
//rewind the buffer
|
||||
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
|
||||
z.AvailableBytesIn = 0;
|
||||
|
||||
@@ -22,11 +22,7 @@ namespace SharpCompress.Factories
|
||||
yield return "ace";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
return AceHeader.IsArchive(stream);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,7 @@ namespace SharpCompress.Factories
|
||||
yield return "arc";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
//You may have to use some(paranoid) checks to ensure that you actually are
|
||||
//processing an ARC file, since other archivers also adopted the idea of putting
|
||||
|
||||
@@ -22,11 +22,7 @@ namespace SharpCompress.Factories
|
||||
yield return "arj";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
return ArjHeader.IsArchive(stream);
|
||||
}
|
||||
|
||||
@@ -51,11 +51,7 @@ public abstract class Factory : IFactory
|
||||
public abstract IEnumerable<string> GetSupportedExtensions();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
);
|
||||
public abstract bool IsArchive(Stream stream, string? password = null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null;
|
||||
@@ -82,7 +78,7 @@ public abstract class Factory : IFactory
|
||||
{
|
||||
long pos = ((IStreamStack)stream).GetPosition();
|
||||
|
||||
if (IsArchive(stream, options.Password, options.BufferSize))
|
||||
if (IsArchive(stream, options.Password))
|
||||
{
|
||||
((IStreamStack)stream).StackSeek(pos);
|
||||
reader = readerFactory.OpenReader(stream, options);
|
||||
|
||||
@@ -40,11 +40,8 @@ public class GZipFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => GZipArchive.IsGZipFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
GZipArchive.IsGZipFile(stream);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -36,11 +36,7 @@ public interface IFactory
|
||||
/// </summary>
|
||||
/// <param name="stream">A stream, pointing to the beginning of the archive.</param>
|
||||
/// <param name="password">optional password</param>
|
||||
bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
);
|
||||
bool IsArchive(Stream stream, string? password = null);
|
||||
|
||||
/// <summary>
|
||||
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
|
||||
|
||||
@@ -29,11 +29,8 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => RarArchive.IsRarFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
RarArchive.IsRarFile(stream);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override FileInfo? GetFilePart(int index, FileInfo part1) =>
|
||||
|
||||
@@ -28,11 +28,8 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => SevenZipArchive.IsSevenZipFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
SevenZipArchive.IsSevenZipFile(stream);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -53,11 +53,8 @@ public class TarFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => TarArchive.IsTarFile(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
TarArchive.IsTarFile(stream);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ internal class ZStandardFactory : Factory
|
||||
yield return "zstd";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = 65536
|
||||
) => ZStandardStream.IsZStandard(stream);
|
||||
public override bool IsArchive(Stream stream, string? password = null) =>
|
||||
ZStandardStream.IsZStandard(stream);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@ public class ZipFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
public override bool IsArchive(Stream stream, string? password = null)
|
||||
{
|
||||
var startPosition = stream.CanSeek ? stream.Position : -1;
|
||||
|
||||
@@ -51,10 +47,10 @@ public class ZipFactory
|
||||
|
||||
if (stream is not SharpCompressStream) // wrap to provide buffer bef
|
||||
{
|
||||
stream = new SharpCompressStream(stream, bufferSize: bufferSize);
|
||||
stream = new SharpCompressStream(stream, bufferSize: Constants.BufferSize);
|
||||
}
|
||||
|
||||
if (ZipArchive.IsZipFile(stream, password, bufferSize))
|
||||
if (ZipArchive.IsZipFile(stream, password))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -69,7 +65,7 @@ public class ZipFactory
|
||||
stream.Position = startPosition;
|
||||
|
||||
//test the zip (last) file of a multipart zip
|
||||
if (ZipArchive.IsZipMulti(stream, password, bufferSize))
|
||||
if (ZipArchive.IsZipMulti(stream, password))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -28,14 +29,25 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(BufferedSubStream));
|
||||
#endif
|
||||
if (disposing) { }
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposing && _cache is not null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_cache);
|
||||
_cache = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private int _cacheOffset;
|
||||
private int _cacheLength;
|
||||
private readonly byte[] _cache = new byte[32 << 10];
|
||||
private byte[]? _cache = ArrayPool<byte>.Shared.Rent(81920);
|
||||
private long origin;
|
||||
private bool _isDisposed;
|
||||
|
||||
private long BytesLeftToRead { get; set; }
|
||||
|
||||
@@ -57,14 +69,26 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
|
||||
private void RefillCache()
|
||||
{
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(BufferedSubStream));
|
||||
}
|
||||
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
|
||||
_cacheOffset = 0;
|
||||
if (count == 0)
|
||||
{
|
||||
_cacheLength = 0;
|
||||
return;
|
||||
}
|
||||
Stream.Position = origin;
|
||||
|
||||
// Only seek if we're not already at the correct position
|
||||
// This avoids expensive seek operations when reading sequentially
|
||||
if (Stream.CanSeek && Stream.Position != origin)
|
||||
{
|
||||
Stream.Position = origin;
|
||||
}
|
||||
|
||||
_cacheLength = Stream.Read(_cache, 0, count);
|
||||
origin += _cacheLength;
|
||||
BytesLeftToRead -= _cacheLength;
|
||||
@@ -72,14 +96,24 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
|
||||
private async ValueTask RefillCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(BufferedSubStream));
|
||||
}
|
||||
|
||||
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
|
||||
_cacheOffset = 0;
|
||||
if (count == 0)
|
||||
{
|
||||
_cacheLength = 0;
|
||||
return;
|
||||
}
|
||||
Stream.Position = origin;
|
||||
// Only seek if we're not already at the correct position
|
||||
// This avoids expensive seek operations when reading sequentially
|
||||
if (Stream.CanSeek && Stream.Position != origin)
|
||||
{
|
||||
Stream.Position = origin;
|
||||
}
|
||||
_cacheLength = await Stream
|
||||
.ReadAsync(_cache, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@@ -102,7 +136,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
|
||||
count = Math.Min(count, _cacheLength - _cacheOffset);
|
||||
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
|
||||
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
|
||||
_cacheOffset += count;
|
||||
}
|
||||
|
||||
@@ -120,7 +154,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
return _cache[_cacheOffset++];
|
||||
return _cache![_cacheOffset++];
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
@@ -143,7 +177,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
|
||||
count = Math.Min(count, _cacheLength - _cacheOffset);
|
||||
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
|
||||
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
|
||||
_cacheOffset += count;
|
||||
}
|
||||
|
||||
@@ -170,7 +204,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
|
||||
count = Math.Min(count, _cacheLength - _cacheOffset);
|
||||
_cache.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
|
||||
_cache!.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
|
||||
_cacheOffset += count;
|
||||
}
|
||||
|
||||
|
||||
@@ -206,11 +206,11 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
// Fill buffer if needed, handling short reads from underlying stream
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = Stream.Read(_buffer!, 0, _bufferSize);
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = FillBuffer(_buffer!, 0, _bufferSize);
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(count, available);
|
||||
@@ -222,11 +222,8 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = Stream.Read(_buffer!, 0, _bufferSize);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = FillBuffer(_buffer!, 0, _bufferSize);
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
@@ -250,6 +247,31 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the buffer by reading from the underlying stream, handling short reads.
|
||||
/// Implements the ReadFully pattern: reads in a loop until buffer is full or EOF is reached.
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to fill</param>
|
||||
/// <param name="offset">Offset in buffer (always 0 in current usage)</param>
|
||||
/// <param name="count">Number of bytes to read</param>
|
||||
/// <returns>Total number of bytes read (may be less than count if EOF is reached)</returns>
|
||||
private int FillBuffer(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Implement ReadFully pattern but return the actual count read
|
||||
// This is the same logic as Utility.ReadFully but returns count instead of bool
|
||||
var total = 0;
|
||||
int read;
|
||||
while ((read = Stream.Read(buffer, offset + total, count - total)) > 0)
|
||||
{
|
||||
total += read;
|
||||
if (total >= count)
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
if (_bufferingEnabled)
|
||||
@@ -257,7 +279,6 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
ValidateBufferState();
|
||||
}
|
||||
|
||||
long orig = _internalPosition;
|
||||
long targetPos;
|
||||
// Calculate the absolute target position based on origin
|
||||
switch (origin)
|
||||
@@ -325,13 +346,12 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
// Fill buffer if needed, handling short reads from underlying stream
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = await Stream
|
||||
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(count, available);
|
||||
@@ -343,13 +363,9 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = await Stream
|
||||
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
@@ -370,6 +386,38 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async version of FillBuffer. Implements the ReadFullyAsync pattern.
|
||||
/// Reads in a loop until buffer is full or EOF is reached.
|
||||
/// </summary>
|
||||
private async Task<int> FillBufferAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Implement ReadFullyAsync pattern but return the actual count read
|
||||
// This is the same logic as Utility.ReadFullyAsync but returns count instead of bool
|
||||
var total = 0;
|
||||
int read;
|
||||
while (
|
||||
(
|
||||
read = await Stream
|
||||
.ReadAsync(buffer, offset + total, count - total, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
) > 0
|
||||
)
|
||||
{
|
||||
total += read;
|
||||
if (total >= count)
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
@@ -400,13 +448,15 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
// Fill buffer if needed, handling short reads from underlying stream
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = await Stream
|
||||
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferMemoryAsync(
|
||||
_buffer.AsMemory(0, _bufferSize),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(buffer.Length, available);
|
||||
@@ -418,13 +468,12 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = await Stream
|
||||
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
_bufferedLength = await FillBufferMemoryAsync(
|
||||
_buffer.AsMemory(0, _bufferSize),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
@@ -443,6 +492,35 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async version of FillBuffer for Memory{byte}. Implements the ReadFullyAsync pattern.
|
||||
/// Reads in a loop until buffer is full or EOF is reached.
|
||||
/// </summary>
|
||||
private async ValueTask<int> FillBufferMemoryAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// Implement ReadFullyAsync pattern but return the actual count read
|
||||
var total = 0;
|
||||
int read;
|
||||
while (
|
||||
(
|
||||
read = await Stream
|
||||
.ReadAsync(buffer.Slice(total), cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
) > 0
|
||||
)
|
||||
{
|
||||
total += read;
|
||||
if (total >= buffer.Length)
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
|
||||
@@ -222,8 +222,26 @@ public class SourceStream : Stream, IStreamStack
|
||||
SetStream(0);
|
||||
while (_prevSize + Current.Length < pos)
|
||||
{
|
||||
_prevSize += Current.Length;
|
||||
SetStream(_stream + 1);
|
||||
var currentLength = Current.Length;
|
||||
_prevSize += currentLength;
|
||||
|
||||
if (!SetStream(_stream + 1))
|
||||
{
|
||||
// No more streams available, cannot seek to requested position
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot seek to position {pos}. End of stream reached at position {_prevSize}."
|
||||
);
|
||||
}
|
||||
|
||||
// Safety check: if we have a zero-length stream and we're still not
|
||||
// making progress toward the target position, we're in an invalid state
|
||||
if (currentLength == 0 && Current.Length == 0)
|
||||
{
|
||||
// Both old and new stream have zero length - cannot make progress
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot seek to position {pos}. Encountered zero-length streams at position {_prevSize}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader
|
||||
{
|
||||
using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
sourceStream.CopyTo(writeStream, 81920);
|
||||
sourceStream.CopyTo(writeStream, Constants.BufferSize);
|
||||
}
|
||||
|
||||
internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
@@ -270,11 +270,15 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ namespace SharpCompress.Readers;
|
||||
|
||||
public class ReaderOptions : OptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The default buffer size for stream operations.
|
||||
/// This value (65536 bytes) is preserved for backward compatibility.
|
||||
/// New code should use Constants.BufferSize instead (81920 bytes), which matches .NET's Stream.CopyTo default.
|
||||
/// </summary>
|
||||
[Obsolete(
|
||||
"Use Constants.BufferSize instead. This constant will be removed in a future version."
|
||||
)]
|
||||
public const int DefaultBufferSize = 0x10000;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,7 +24,7 @@ public class ReaderOptions : OptionsBase
|
||||
|
||||
public bool DisableCheckIncomplete { get; set; }
|
||||
|
||||
public int BufferSize { get; set; } = DefaultBufferSize;
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Provide a hint for the extension of the archive being read, can speed up finding the correct decoder. Should be without the leading period in the form like: tar.gz or zip
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>SharpCompress - Pure C# Decompression/Compression</AssemblyTitle>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<VersionPrefix>0.42.0</VersionPrefix>
|
||||
<AssemblyVersion>0.42.0</AssemblyVersion>
|
||||
<FileVersion>0.42.0</FileVersion>
|
||||
<VersionPrefix>0.0.0</VersionPrefix>
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
<Authors>Adam Hathcock</Authors>
|
||||
<TargetFrameworks>net48;net8.0;net10.0</TargetFrameworks>
|
||||
<TargetFrameworks>net48;netstandard2.0;net8.0;net10.0</TargetFrameworks>
|
||||
<AssemblyName>SharpCompress</AssemblyName>
|
||||
<AssemblyOriginatorKeyFile>../../SharpCompress.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
@@ -17,7 +17,7 @@
|
||||
<Copyright>Copyright (c) 2025 Adam Hathcock</Copyright>
|
||||
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
|
||||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||
<Description>SharpCompress is a compression library for NET 4.8/NET 8.0/NET 10.0 that can unrar, decompress 7zip, decompress xz, zip/unzip, tar/untar lzip/unlzip, bzip2/unbzip2 and gzip/ungzip with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip is implemented.</Description>
|
||||
<Description>SharpCompress is a compression library for NET 4.8/NET Standard 2.0/NET 8.0/NET 10.0 that can unrar, decompress 7zip, decompress xz, zip/unzip, tar/untar lzip/unlzip, bzip2/unbzip2 and gzip/ungzip with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip is implemented.</Description>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<DebugType>embedded</DebugType>
|
||||
@@ -30,25 +30,13 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
|
||||
<PackageReference Include="Microsoft.NET.ILLink.Tasks" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||
<PackageReference Include="System.Buffers" />
|
||||
<PackageReference Include="System.Memory" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace SharpCompress;
|
||||
|
||||
internal static class Utility
|
||||
{
|
||||
//80kb is a good industry standard temporary buffer size
|
||||
private const int TEMP_BUFFER_SIZE = 81920;
|
||||
private static readonly HashSet<char> invalidChars = new(Path.GetInvalidFileNameChars());
|
||||
|
||||
public static ReadOnlyCollection<T> ToReadOnly<T>(this IList<T> items) => new(items);
|
||||
@@ -151,7 +149,7 @@ internal static class Utility
|
||||
|
||||
public static long TransferTo(this Stream source, Stream destination, long maxLength)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
var array = ArrayPool<byte>.Shared.Rent(Common.Constants.BufferSize);
|
||||
try
|
||||
{
|
||||
var maxReadSize = array.Length;
|
||||
@@ -190,7 +188,7 @@ internal static class Utility
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
var array = ArrayPool<byte>.Shared.Rent(Common.Constants.BufferSize);
|
||||
try
|
||||
{
|
||||
var maxReadSize = array.Length;
|
||||
@@ -268,7 +266,7 @@ internal static class Utility
|
||||
return;
|
||||
}
|
||||
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
var array = ArrayPool<byte>.Shared.Rent(Common.Constants.BufferSize);
|
||||
try
|
||||
{
|
||||
while (advanceAmount > 0)
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class GZipWriter : AbstractWriter
|
||||
stream.FileName = filename;
|
||||
stream.LastModified = modificationTime;
|
||||
var progressStream = WrapWithProgress(source, filename);
|
||||
progressStream.CopyTo(stream);
|
||||
progressStream.CopyTo(stream, Constants.BufferSize);
|
||||
_wroteToStream = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Compressors.PPMd;
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
using SharpCompress.IO;
|
||||
using Constants = SharpCompress.Common.Constants;
|
||||
|
||||
namespace SharpCompress.Writers.Zip;
|
||||
|
||||
@@ -87,7 +88,7 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
using var output = WriteToStream(entryPath, zipWriterEntryOptions);
|
||||
var progressStream = WrapWithProgress(source, entryPath);
|
||||
progressStream.CopyTo(output);
|
||||
progressStream.CopyTo(output, Constants.BufferSize);
|
||||
}
|
||||
|
||||
public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options)
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
@@ -60,8 +60,8 @@
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48": {
|
||||
"type": "Transitive",
|
||||
@@ -70,8 +70,8 @@
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
@@ -97,60 +97,192 @@
|
||||
"contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w=="
|
||||
}
|
||||
},
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
|
||||
"contentHash": "vFuwSLj9QJBbNR0NeNO4YVASUbokxs+i/xbuu8B+Fs4FAZg5QaFa6eGrMaRqTzzNI5tAb97T7BhSxtLckFyiRA==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.6.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.6.1, )",
|
||||
"resolved": "4.6.1",
|
||||
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.6.3, )",
|
||||
"resolved": "4.6.3",
|
||||
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.6.1",
|
||||
"System.Numerics.Vectors": "4.6.1",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "QLP54mIATaBpjGlsZIxga38VPk1G9js0Kw651B+bvrXi2kSgGZYrxJSpM3whhTZCBK4HEBHX3fzfDQMw7CXHGQ==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.6.3",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.6.1",
|
||||
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.1.2",
|
||||
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.6.3",
|
||||
"contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.2, )",
|
||||
"resolved": "10.0.2",
|
||||
"contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
|
||||
"requested": "[8.0.23, )",
|
||||
"resolved": "8.0.23",
|
||||
"contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,25 @@
|
||||
"JetBrains.Profiler.Api": "1.4.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.3, )",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"JetBrains.FormatRipper": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.4.0",
|
||||
@@ -33,6 +52,21 @@
|
||||
"JetBrains.HabitatDetector": "1.4.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"sharpcompress": {
|
||||
"type": "Project"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
@@ -31,8 +31,8 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public ForwardOnlyStream(Stream stream, int bufferSize = ReaderOptions.DefaultBufferSize)
|
||||
: base(stream, bufferSize: bufferSize)
|
||||
public ForwardOnlyStream(Stream stream, int? bufferSize = null)
|
||||
: base(stream, bufferSize: bufferSize ?? Constants.BufferSize)
|
||||
{
|
||||
this.stream = stream;
|
||||
#if DEBUG_STREAMS
|
||||
|
||||
73
tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs
Normal file
73
tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// A stream wrapper that throws NotSupportedException on Flush() calls.
|
||||
/// This is used to test that archive iteration handles streams that don't support flushing.
|
||||
/// </summary>
|
||||
public class ThrowOnFlushStream : Stream
|
||||
{
|
||||
private readonly Stream inner;
|
||||
|
||||
public ThrowOnFlushStream(Stream inner)
|
||||
{
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
public override bool CanRead => inner.CanRead;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => throw new NotSupportedException("Flush not supported");
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("FlushAsync not supported");
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
inner.Read(buffer, offset, count);
|
||||
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => inner.ReadAsync(buffer, cancellationToken);
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
inner.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -717,4 +717,37 @@ public class RarArchiveTests : ArchiveTests
|
||||
// Verify the exception message matches our expectation
|
||||
Assert.Contains("unpacked file size does not match header", exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for malformed RAR archives that previously caused infinite loops.
|
||||
/// This test verifies that attempting to read entries from a potentially malformed
|
||||
/// 512-byte RAR archive throws an InvalidOperationException instead of looping infinitely.
|
||||
/// See: https://github.com/adamhathcock/sharpcompress/issues/1176
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Rar_MalformedArchive_NoInfiniteLoop()
|
||||
{
|
||||
var testFile = "Rar.malformed_512byte.rar";
|
||||
var readerOptions = new ReaderOptions { LookForHeader = true };
|
||||
|
||||
// This should throw InvalidOperationException, not hang in an infinite loop
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var fileStream = File.Open(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, testFile),
|
||||
FileMode.Open
|
||||
);
|
||||
using var archive = RarArchive.Open(fileStream, readerOptions);
|
||||
|
||||
// Attempting to enumerate entries should throw an exception
|
||||
// instead of looping infinitely
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
// This line should not be reached due to the exception
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the exception is related to seeking beyond available data
|
||||
Assert.Contains("Cannot seek to position", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,4 +251,98 @@ public class SevenZipArchiveTests : ArchiveTests
|
||||
);
|
||||
Assert.False(nonSolidArchive.IsSolid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_Solid_ExtractAllEntries_Contiguous()
|
||||
{
|
||||
// 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");
|
||||
using var archive = SevenZipArchive.Open(testArchive);
|
||||
Assert.True(archive.IsSolid);
|
||||
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 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");
|
||||
using var archive = SevenZipArchive.Open(testArchive);
|
||||
Assert.True(archive.IsSolid);
|
||||
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
|
||||
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 (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
// Extract the entry to trigger GetEntryStream
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<AssemblyOriginatorKeyFile>SharpCompress.Test.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0|AnyCPU'">
|
||||
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' ">
|
||||
<DefineConstants>$(DefineConstants);LEGACY_DOTNET</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
|
||||
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
|
||||
@@ -23,9 +23,8 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(VersionlessImplicitFrameworkDefine)' != 'NETFRAMEWORK' ">
|
||||
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
|
||||
<PackageReference Include="Mono.Posix.NETStandard" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Streams;
|
||||
@@ -64,7 +65,14 @@ public class SharpCompressStreamTests
|
||||
{
|
||||
createData(ms);
|
||||
|
||||
using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000))
|
||||
using (
|
||||
SharpCompressStream scs = new SharpCompressStream(
|
||||
new ForwardOnlyStream(ms),
|
||||
true,
|
||||
false,
|
||||
0x10000
|
||||
)
|
||||
)
|
||||
{
|
||||
IStreamStack stack = (IStreamStack)scs;
|
||||
|
||||
@@ -89,4 +97,25 @@ public class SharpCompressStreamTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BufferedSubStream_DoubleDispose_DoesNotCorruptArrayPool()
|
||||
{
|
||||
// This test verifies that calling Dispose multiple times on BufferedSubStream
|
||||
// doesn't return the same array to the pool twice, which would cause pool corruption
|
||||
byte[] data = new byte[0x10000];
|
||||
using (MemoryStream ms = new MemoryStream(data))
|
||||
{
|
||||
var stream = new BufferedSubStream(ms, 0, data.Length);
|
||||
|
||||
// First disposal
|
||||
stream.Dispose();
|
||||
|
||||
// Second disposal should not throw or corrupt the pool
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
// If we got here without an exception, the test passed
|
||||
Assert.True(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,4 +251,106 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
}
|
||||
Assert.Equal(8, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_Deflate_Async()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.DisposeAsync() should not throw NotSupportedException
|
||||
// when FlushAsync() fails on non-seekable streams (Deflate compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal FlushAsync() fails
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
// DisposeAsync should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_LZMA_Async()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.DisposeAsync() should not throw NotSupportedException
|
||||
// when FlushAsync() fails on non-seekable streams (LZMA compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal FlushAsync() fails
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
// DisposeAsync should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate_Async()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA_Async()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,4 +436,98 @@ public class ZipReaderTests : ReaderTests
|
||||
Assert.Equal(archiveKeys.OrderBy(k => k), readerKeys.OrderBy(k => k));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_Deflate()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.Dispose() should not throw NotSupportedException
|
||||
// when Flush() fails on non-seekable streams (Deflate compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal Flush() fails
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
entryStream.Read(buffer, 0, buffer.Length);
|
||||
// Dispose should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_LZMA()
|
||||
{
|
||||
// Since version 0.41.0: EntryStream.Dispose() should not throw NotSupportedException
|
||||
// when Flush() fails on non-seekable streams (LZMA compression)
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw, even if internal Flush() fails
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
entryStream.Read(buffer, 0, buffer.Length);
|
||||
// Dispose should not throw NotSupportedException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA()
|
||||
{
|
||||
// Regression test: since 0.41.0, archive iteration would silently break
|
||||
// when the input stream throws NotSupportedException in Flush().
|
||||
// Only the first entry would be returned, then iteration would stop without exception.
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using Stream stream = new ThrowOnFlushStream(fileStream);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var count = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should iterate through all entries, not just the first one
|
||||
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
|
||||
}
|
||||
}
|
||||
|
||||
117
tests/SharpCompress.Test/Zip/ZipShortReadTests.cs
Normal file
117
tests/SharpCompress.Test/Zip/ZipShortReadTests.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ZIP reading with streams that return short reads.
|
||||
/// Reproduces the regression where ZIP parsing fails depending on Stream.Read chunking patterns.
|
||||
/// </summary>
|
||||
public class ZipShortReadTests : ReaderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// A non-seekable stream that returns controlled short reads.
|
||||
/// Simulates real-world network/multipart streams that legally return fewer bytes than requested.
|
||||
/// </summary>
|
||||
private sealed class PatternReadStream : Stream
|
||||
{
|
||||
private readonly MemoryStream _inner;
|
||||
private readonly int _firstReadSize;
|
||||
private readonly int _chunkSize;
|
||||
private bool _firstReadDone;
|
||||
|
||||
public PatternReadStream(byte[] bytes, int firstReadSize, int chunkSize)
|
||||
{
|
||||
_inner = new MemoryStream(bytes, writable: false);
|
||||
_firstReadSize = firstReadSize;
|
||||
_chunkSize = chunkSize;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int limit = !_firstReadDone ? _firstReadSize : _chunkSize;
|
||||
_firstReadDone = true;
|
||||
|
||||
int toRead = Math.Min(count, limit);
|
||||
return _inner.Read(buffer, offset, toRead);
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => throw new NotSupportedException();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that ZIP reading works correctly with short reads on non-seekable streams.
|
||||
/// Uses a test archive and different chunking patterns.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("Zip.deflate.zip", 1000, 4096)]
|
||||
[InlineData("Zip.deflate.zip", 999, 4096)]
|
||||
[InlineData("Zip.deflate.zip", 100, 4096)]
|
||||
[InlineData("Zip.deflate.zip", 50, 512)]
|
||||
[InlineData("Zip.deflate.zip", 1, 1)] // Extreme case: 1 byte at a time
|
||||
[InlineData("Zip.deflate.dd.zip", 1000, 4096)]
|
||||
[InlineData("Zip.deflate.dd.zip", 999, 4096)]
|
||||
[InlineData("Zip.zip64.zip", 3816, 4096)]
|
||||
[InlineData("Zip.zip64.zip", 3815, 4096)] // Similar to the issue pattern
|
||||
public void Zip_Reader_Handles_Short_Reads(string zipFile, int firstReadSize, int chunkSize)
|
||||
{
|
||||
// Use an existing test ZIP file
|
||||
var zipPath = Path.Combine(TEST_ARCHIVES_PATH, zipFile);
|
||||
if (!File.Exists(zipPath))
|
||||
{
|
||||
return; // Skip if file doesn't exist
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(zipPath);
|
||||
|
||||
// Baseline with MemoryStream (seekable, no short reads)
|
||||
var baseline = ReadEntriesFromStream(new MemoryStream(bytes, writable: false));
|
||||
Assert.NotEmpty(baseline);
|
||||
|
||||
// Non-seekable stream with controlled short read pattern
|
||||
var chunked = ReadEntriesFromStream(new PatternReadStream(bytes, firstReadSize, chunkSize));
|
||||
Assert.Equal(baseline, chunked);
|
||||
}
|
||||
|
||||
private List<string> ReadEntriesFromStream(Stream stream)
|
||||
{
|
||||
var names = new List<string>();
|
||||
using var reader = ReaderFactory.Open(stream, new ReaderOptions { LeaveStreamOpen = true });
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (reader.Entry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
names.Add(reader.Entry.Key!);
|
||||
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
entryStream.CopyTo(Stream.Null);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,16 @@
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Mono.Posix.NETStandard": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
@@ -55,6 +65,11 @@
|
||||
"Microsoft.TestPlatform.ObjectModel": "17.13.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.0.1",
|
||||
@@ -65,6 +80,11 @@
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.13.0",
|
||||
@@ -222,6 +242,16 @@
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Mono.Posix.NETStandard": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
@@ -245,6 +275,11 @@
|
||||
"resolved": "3.1.5",
|
||||
"contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA=="
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.0.1",
|
||||
@@ -255,6 +290,11 @@
|
||||
"resolved": "1.0.3",
|
||||
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.0.1",
|
||||
|
||||
BIN
tests/TestArchives/Archives/Rar.malformed_512byte.rar
Normal file
BIN
tests/TestArchives/Archives/Rar.malformed_512byte.rar
Normal file
Binary file not shown.
Reference in New Issue
Block a user