mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-08 13:34:57 +00:00
Compare commits
55 Commits
copilot/ad
...
release
| 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 | ||
|
|
97879f18b6 | ||
|
|
d74454f7e9 |
@@ -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)
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace SharpCompress.Common.Ace.Headers
|
||||
}
|
||||
}
|
||||
|
||||
public AceFileHeader(IArchiveEncoding archiveEncoding)
|
||||
public AceFileHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.FILE) { }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -31,13 +31,13 @@ namespace SharpCompress.Common.Ace.Headers
|
||||
(byte)'*',
|
||||
];
|
||||
|
||||
public AceHeader(IArchiveEncoding archiveEncoding, AceHeaderType type)
|
||||
public AceHeader(ArchiveEncoding archiveEncoding, AceHeaderType type)
|
||||
{
|
||||
AceHeaderType = type;
|
||||
ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
public IArchiveEncoding ArchiveEncoding { get; }
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public AceHeaderType AceHeaderType { get; }
|
||||
|
||||
public ushort HeaderFlags { get; set; }
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace SharpCompress.Common.Ace.Headers
|
||||
public List<byte> Comment { get; set; } = new();
|
||||
public byte AceVersion { get; private set; }
|
||||
|
||||
public AceMainHeader(IArchiveEncoding archiveEncoding)
|
||||
public AceMainHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding, AceHeaderType.MAIN) { }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Arc
|
||||
{
|
||||
public class ArcEntryHeader
|
||||
{
|
||||
public IArchiveEncoding ArchiveEncoding { get; }
|
||||
public ArchiveEncoding ArchiveEncoding { get; }
|
||||
public CompressionType CompressionMethod { get; private set; }
|
||||
public string? Name { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
@@ -16,7 +16,7 @@ namespace SharpCompress.Common.Arc
|
||||
public long OriginalSize { get; private set; }
|
||||
public long DataStartPosition { get; private set; }
|
||||
|
||||
public ArcEntryHeader(IArchiveEncoding archiveEncoding)
|
||||
public ArcEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
{
|
||||
this.ArchiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,55 @@ using System.Text;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public class ArchiveEncoding : IArchiveEncoding
|
||||
public class ArchiveEncoding
|
||||
{
|
||||
public Encoding Default { get; set; } = Encoding.Default;
|
||||
public Encoding Password { get; set; } = Encoding.Default;
|
||||
public Encoding UTF8 { get; set; } = Encoding.UTF8;
|
||||
/// <summary>
|
||||
/// Default encoding to use when archive format doesn't specify one.
|
||||
/// </summary>
|
||||
public Encoding? Default { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898.
|
||||
/// </summary>
|
||||
public Encoding? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this encoding when you want to force it for all encoding operations.
|
||||
/// </summary>
|
||||
public Encoding? Forced { get; set; }
|
||||
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this when you want to use a custom method for all decoding operations.
|
||||
/// </summary>
|
||||
/// <returns>string Func(bytes, index, length)</returns>
|
||||
public Func<byte[], int, int, string>? CustomDecoder { get; set; }
|
||||
|
||||
public ArchiveEncoding()
|
||||
: this(Encoding.Default, Encoding.Default) { }
|
||||
|
||||
public ArchiveEncoding(Encoding def, Encoding password)
|
||||
{
|
||||
Default = def;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
static ArchiveEncoding() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
#endif
|
||||
|
||||
public string Decode(byte[] bytes) => Decode(bytes, 0, bytes.Length);
|
||||
|
||||
public string Decode(byte[] bytes, int start, int length) =>
|
||||
GetDecoder().Invoke(bytes, start, length);
|
||||
|
||||
public string DecodeUTF8(byte[] bytes) => Encoding.UTF8.GetString(bytes, 0, bytes.Length);
|
||||
|
||||
public byte[] Encode(string str) => GetEncoding().GetBytes(str);
|
||||
|
||||
public Encoding GetEncoding() => Forced ?? Default ?? Encoding.UTF8;
|
||||
|
||||
public Encoding GetPasswordEncoding() => Password ?? Encoding.UTF8;
|
||||
|
||||
public Func<byte[], int, int, string> GetDecoder() =>
|
||||
CustomDecoder ?? ((bytes, index, count) => GetEncoding().GetString(bytes, index, count));
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of encoding to use.
|
||||
/// </summary>
|
||||
public enum EncodingType
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses the default encoding.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Uses UTF-8 encoding.
|
||||
/// </summary>
|
||||
UTF8,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for archive encoding.
|
||||
/// </summary>
|
||||
public static class ArchiveEncodingExtensions
|
||||
{
|
||||
#if !NETFRAMEWORK
|
||||
/// <summary>
|
||||
/// Registers the code pages encoding provider.
|
||||
/// </summary>
|
||||
static ArchiveEncodingExtensions() =>
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
#endif
|
||||
|
||||
extension(IArchiveEncoding encoding)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the encoding based on the archive encoding settings.
|
||||
/// </summary>
|
||||
/// <param name="useUtf8">Whether to use UTF-8.</param>
|
||||
/// <returns>The encoding.</returns>
|
||||
public Encoding GetEncoding(bool useUtf8 = false) =>
|
||||
encoding.Forced ?? (useUtf8 ? encoding.UTF8 : encoding.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the decoder function for the archive encoding.
|
||||
/// </summary>
|
||||
/// <returns>The decoder function.</returns>
|
||||
public Func<byte[], int, int, EncodingType, string> GetDecoder() =>
|
||||
encoding.CustomDecoder
|
||||
?? (
|
||||
(bytes, index, count, type) =>
|
||||
encoding.GetEncoding(type == EncodingType.UTF8).GetString(bytes, index, count)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string using the default encoding.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to encode.</param>
|
||||
/// <returns>The encoded bytes.</returns>
|
||||
public byte[] Encode(string str) => encoding.Default.GetBytes(str);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes bytes using the specified encoding type.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to decode.</param>
|
||||
/// <param name="type">The encoding type.</param>
|
||||
/// <returns>The decoded string.</returns>
|
||||
public string Decode(byte[] bytes, EncodingType type = EncodingType.Default) =>
|
||||
encoding.Decode(bytes, 0, bytes.Length, type);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a portion of bytes using the specified encoding type.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes to decode.</param>
|
||||
/// <param name="start">The start index.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="type">The encoding type.</param>
|
||||
/// <returns>The decoded string.</returns>
|
||||
public string Decode(
|
||||
byte[] bytes,
|
||||
int start,
|
||||
int length,
|
||||
EncodingType type = EncodingType.Default
|
||||
) => encoding.GetDecoder()(bytes, start, length, type);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -4,9 +4,9 @@ namespace SharpCompress.Common;
|
||||
|
||||
public abstract class FilePart
|
||||
{
|
||||
protected FilePart(IArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
|
||||
protected FilePart(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
|
||||
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
internal abstract string? FilePartName { get; }
|
||||
public int Index { get; set; }
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class GZipFilePart : FilePart
|
||||
private string? _name;
|
||||
private readonly Stream _stream;
|
||||
|
||||
internal GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding)
|
||||
internal GZipFilePart(Stream stream, ArchiveEncoding archiveEncoding)
|
||||
: base(archiveEncoding)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the encoding settings for archives.
|
||||
/// </summary>
|
||||
public interface IArchiveEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Default encoding to use when archive format doesn't specify one. Required and defaults to Encoding.Default.
|
||||
/// </summary>
|
||||
public Encoding Default { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898. Required and defaults to Encoding.Default.
|
||||
/// </summary>
|
||||
public Encoding Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default encoding to use when archive format specifies UTF-8 encoding. Required and defaults to Encoding.UTF8.
|
||||
/// </summary>
|
||||
public Encoding UTF8 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this encoding when you want to force it for all encoding operations.
|
||||
/// </summary>
|
||||
public Encoding? Forced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this when you want to use a custom method for all decoding operations.
|
||||
/// </summary>
|
||||
/// <returns>string Func(bytes, index, length, EncodingType)</returns>
|
||||
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
|
||||
}
|
||||
@@ -7,5 +7,5 @@ public class OptionsBase
|
||||
/// </summary>
|
||||
public bool LeaveStreamOpen { get; set; } = true;
|
||||
|
||||
public IArchiveEncoding ArchiveEncoding { get; set; } = new ArchiveEncoding();
|
||||
public ArchiveEncoding ArchiveEncoding { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ internal class RarHeader : IRarHeader
|
||||
internal static RarHeader? TryReadBase(
|
||||
RarCrcBinaryReader reader,
|
||||
bool isRar5,
|
||||
IArchiveEncoding archiveEncoding
|
||||
ArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
try
|
||||
@@ -26,7 +26,7 @@ internal class RarHeader : IRarHeader
|
||||
}
|
||||
}
|
||||
|
||||
private RarHeader(RarCrcBinaryReader reader, bool isRar5, IArchiveEncoding archiveEncoding)
|
||||
private RarHeader(RarCrcBinaryReader reader, bool isRar5, ArchiveEncoding archiveEncoding)
|
||||
{
|
||||
_headerType = HeaderType.Null;
|
||||
_isRar5 = isRar5;
|
||||
@@ -115,7 +115,7 @@ internal class RarHeader : IRarHeader
|
||||
|
||||
protected int HeaderSize { get; }
|
||||
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Extra header size.
|
||||
|
||||
@@ -15,7 +15,7 @@ internal class SevenZipFilePart : FilePart
|
||||
ArchiveDatabase database,
|
||||
int index,
|
||||
CFileItem fileEntry,
|
||||
IArchiveEncoding archiveEncoding
|
||||
ArchiveEncoding archiveEncoding
|
||||
)
|
||||
: base(archiveEncoding)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class TarHeader
|
||||
internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public TarHeader(
|
||||
IArchiveEncoding archiveEncoding,
|
||||
ArchiveEncoding archiveEncoding,
|
||||
TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK
|
||||
)
|
||||
{
|
||||
@@ -30,7 +30,7 @@ internal sealed class TarHeader
|
||||
internal DateTime LastModifiedTime { get; set; }
|
||||
internal EntryType EntryType { get; set; }
|
||||
internal Stream? PackedStream { get; set; }
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
internal const int BLOCK_SIZE = 512;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ public class TarEntry : Entry
|
||||
StreamingMode mode,
|
||||
Stream stream,
|
||||
CompressionType compressionType,
|
||||
IArchiveEncoding archiveEncoding
|
||||
ArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
foreach (var header in TarHeaderFactory.ReadHeader(mode, stream, archiveEncoding))
|
||||
|
||||
@@ -10,7 +10,7 @@ internal static class TarHeaderFactory
|
||||
internal static IEnumerable<TarHeader?> ReadHeader(
|
||||
StreamingMode mode,
|
||||
Stream stream,
|
||||
IArchiveEncoding archiveEncoding
|
||||
ArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
while (true)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace SharpCompress.Common.Zip.Headers;
|
||||
|
||||
internal class DirectoryEntryHeader : ZipFileEntry
|
||||
{
|
||||
public DirectoryEntryHeader(IArchiveEncoding archiveEncoding)
|
||||
public DirectoryEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(ZipHeaderType.DirectoryEntry, archiveEncoding) { }
|
||||
|
||||
internal override void Read(BinaryReader reader)
|
||||
@@ -41,8 +41,8 @@ internal class DirectoryEntryHeader : ZipFileEntry
|
||||
|
||||
if (Flags.HasFlag(HeaderFlags.Efs))
|
||||
{
|
||||
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
|
||||
Comment = ArchiveEncoding.Decode(comment, EncodingType.UTF8);
|
||||
Name = ArchiveEncoding.DecodeUTF8(name);
|
||||
Comment = ArchiveEncoding.DecodeUTF8(comment);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace SharpCompress.Common.Zip.Headers;
|
||||
|
||||
internal class LocalEntryHeader : ZipFileEntry
|
||||
{
|
||||
public LocalEntryHeader(IArchiveEncoding archiveEncoding)
|
||||
public LocalEntryHeader(ArchiveEncoding archiveEncoding)
|
||||
: base(ZipHeaderType.LocalEntry, archiveEncoding) { }
|
||||
|
||||
internal override void Read(BinaryReader reader)
|
||||
@@ -33,7 +33,7 @@ internal class LocalEntryHeader : ZipFileEntry
|
||||
|
||||
if (Flags.HasFlag(HeaderFlags.Efs))
|
||||
{
|
||||
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
|
||||
Name = ArchiveEncoding.DecodeUTF8(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Zip.Headers;
|
||||
|
||||
internal abstract class ZipFileEntry : ZipHeader
|
||||
{
|
||||
protected ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding)
|
||||
protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding)
|
||||
: base(type)
|
||||
{
|
||||
Extra = new List<ExtraData>();
|
||||
@@ -30,7 +30,7 @@ internal abstract class ZipFileEntry : ZipHeader
|
||||
|
||||
internal Stream? PackedStream { get; set; }
|
||||
|
||||
internal IArchiveEncoding ArchiveEncoding { get; }
|
||||
internal ArchiveEncoding ArchiveEncoding { get; }
|
||||
|
||||
internal string? Name { get; set; }
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
|
||||
@@ -9,9 +8,9 @@ internal class PkwareTraditionalEncryptionData
|
||||
{
|
||||
private static readonly CRC32 CRC32 = new();
|
||||
private readonly uint[] _keys = { 0x12345678, 0x23456789, 0x34567890 };
|
||||
private readonly IArchiveEncoding _archiveEncoding;
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
|
||||
private PkwareTraditionalEncryptionData(string password, IArchiveEncoding archiveEncoding)
|
||||
private PkwareTraditionalEncryptionData(string password, ArchiveEncoding archiveEncoding)
|
||||
{
|
||||
_archiveEncoding = archiveEncoding;
|
||||
Initialize(password);
|
||||
@@ -48,44 +47,6 @@ internal class PkwareTraditionalEncryptionData
|
||||
return encryptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PkwareTraditionalEncryptionData instance for writing encrypted data.
|
||||
/// </summary>
|
||||
/// <param name="password">The password to use for encryption.</param>
|
||||
/// <param name="archiveEncoding">The archive encoding.</param>
|
||||
/// <returns>A new encryption data instance.</returns>
|
||||
public static PkwareTraditionalEncryptionData ForWrite(
|
||||
string password,
|
||||
IArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
return new PkwareTraditionalEncryptionData(password, archiveEncoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the 12-byte encryption header required for PKWARE traditional encryption.
|
||||
/// </summary>
|
||||
/// <param name="crc">The CRC32 of the uncompressed file data, or the last modified time high byte if using data descriptors.</param>
|
||||
/// <param name="lastModifiedTime">The last modified time (used as verification byte when CRC is unknown).</param>
|
||||
/// <returns>The encrypted 12-byte header.</returns>
|
||||
public byte[] GenerateEncryptionHeader(uint crc, ushort lastModifiedTime)
|
||||
{
|
||||
var header = new byte[12];
|
||||
|
||||
// Fill first 11 bytes with random data
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(header, 0, 11);
|
||||
}
|
||||
|
||||
// The last byte is the verification byte - high byte of CRC, or high byte of lastModifiedTime
|
||||
// When streaming (UsePostDataDescriptor), we use the time as verification
|
||||
header[11] = (byte)((crc >> 24) & 0xff);
|
||||
|
||||
// Encrypt the header
|
||||
return Encrypt(header, header.Length);
|
||||
}
|
||||
|
||||
public byte[] Decrypt(byte[] cipherText, int length)
|
||||
{
|
||||
if (length > cipherText.Length)
|
||||
@@ -142,7 +103,7 @@ internal class PkwareTraditionalEncryptionData
|
||||
|
||||
internal byte[] StringToByteArray(string value)
|
||||
{
|
||||
var a = _archiveEncoding.Password.GetBytes(value);
|
||||
var a = _archiveEncoding.GetPasswordEncoding().GetBytes(value);
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
|
||||
private const int MAX_SEARCH_LENGTH_FOR_EOCD = 65557;
|
||||
private bool _zip64;
|
||||
|
||||
internal SeekableZipHeaderFactory(string? password, IArchiveEncoding archiveEncoding)
|
||||
internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding)
|
||||
: base(StreamingMode.Seekable, password, archiveEncoding) { }
|
||||
|
||||
internal IEnumerable<ZipHeader> ReadSeekableHeader(Stream stream)
|
||||
|
||||
@@ -13,7 +13,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
|
||||
internal StreamingZipHeaderFactory(
|
||||
string? password,
|
||||
IArchiveEncoding archiveEncoding,
|
||||
ArchiveEncoding archiveEncoding,
|
||||
IEnumerable<ZipEntry>? entries
|
||||
)
|
||||
: base(StreamingMode.Streaming, password, archiveEncoding) => _entries = entries;
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Stream that encrypts data using WinZip AES encryption and writes to an underlying stream.
|
||||
/// </summary>
|
||||
internal class WinzipAesEncryptionStream : Stream
|
||||
{
|
||||
private const int BLOCK_SIZE_IN_BYTES = 16;
|
||||
private const int RFC2898_ITERATIONS = 1000;
|
||||
private const int AUTH_CODE_LENGTH = 10;
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly SymmetricAlgorithm _cipher;
|
||||
private readonly ICryptoTransform _transform;
|
||||
private readonly HMACSHA1 _hmac;
|
||||
private readonly byte[] _counter = new byte[BLOCK_SIZE_IN_BYTES];
|
||||
private readonly byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES];
|
||||
private int _nonce = 1;
|
||||
private bool _isDisposed;
|
||||
|
||||
internal WinzipAesEncryptionStream(Stream stream, string password, WinzipAesKeySize keySize)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
// Generate salt
|
||||
var saltLength = GetSaltLength(keySize);
|
||||
var salt = new byte[saltLength];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(salt);
|
||||
}
|
||||
|
||||
// Derive keys using PBKDF2
|
||||
var keyLength = GetKeyLength(keySize);
|
||||
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
|
||||
var keyBytes = rfc2898.GetBytes(keyLength);
|
||||
var ivBytes = rfc2898.GetBytes(keyLength);
|
||||
var passwordVerifyValue = rfc2898.GetBytes(2);
|
||||
#elif NET10_0_OR_GREATER
|
||||
var derivedKeySize = (keyLength * 2) + 2;
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
var derivedKey = Rfc2898DeriveBytes.Pbkdf2(
|
||||
passwordBytes,
|
||||
salt,
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1,
|
||||
derivedKeySize
|
||||
);
|
||||
var keyBytes = derivedKey.AsSpan(0, keyLength).ToArray();
|
||||
var ivBytes = derivedKey.AsSpan(keyLength, keyLength).ToArray();
|
||||
var passwordVerifyValue = derivedKey.AsSpan(keyLength * 2, 2).ToArray();
|
||||
#else
|
||||
var rfc2898 = new Rfc2898DeriveBytes(
|
||||
password,
|
||||
salt,
|
||||
RFC2898_ITERATIONS,
|
||||
HashAlgorithmName.SHA1
|
||||
);
|
||||
var keyBytes = rfc2898.GetBytes(keyLength);
|
||||
var ivBytes = rfc2898.GetBytes(keyLength);
|
||||
var passwordVerifyValue = rfc2898.GetBytes(2);
|
||||
#endif
|
||||
|
||||
// Initialize cipher
|
||||
_cipher = CreateCipher(keyBytes);
|
||||
var iv = new byte[BLOCK_SIZE_IN_BYTES];
|
||||
_transform = _cipher.CreateEncryptor(keyBytes, iv);
|
||||
|
||||
// Initialize HMAC for authentication
|
||||
_hmac = new HMACSHA1(ivBytes);
|
||||
|
||||
// Write salt and password verification value
|
||||
_stream.Write(salt, 0, salt.Length);
|
||||
_stream.Write(passwordVerifyValue, 0, passwordVerifyValue.Length);
|
||||
}
|
||||
|
||||
private static int GetSaltLength(WinzipAesKeySize keySize) =>
|
||||
keySize switch
|
||||
{
|
||||
WinzipAesKeySize.KeySize128 => 8,
|
||||
WinzipAesKeySize.KeySize192 => 12,
|
||||
WinzipAesKeySize.KeySize256 => 16,
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
private static int GetKeyLength(WinzipAesKeySize keySize) =>
|
||||
keySize switch
|
||||
{
|
||||
WinzipAesKeySize.KeySize128 => 16,
|
||||
WinzipAesKeySize.KeySize192 => 24,
|
||||
WinzipAesKeySize.KeySize256 => 32,
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
private static SymmetricAlgorithm CreateCipher(byte[] keyBytes)
|
||||
{
|
||||
var cipher = Aes.Create();
|
||||
cipher.BlockSize = BLOCK_SIZE_IN_BYTES * 8;
|
||||
cipher.KeySize = keyBytes.Length * 8;
|
||||
cipher.Mode = CipherMode.ECB;
|
||||
cipher.Padding = PaddingMode.None;
|
||||
return cipher;
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => _stream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
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)
|
||||
{
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var encrypted = EncryptData(buffer, offset, count);
|
||||
_hmac.TransformBlock(encrypted, 0, encrypted.Length, encrypted, 0);
|
||||
_stream.Write(encrypted, 0, encrypted.Length);
|
||||
}
|
||||
|
||||
private byte[] EncryptData(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var result = new byte[count];
|
||||
var posn = 0;
|
||||
|
||||
while (posn < count)
|
||||
{
|
||||
var blockSize = Math.Min(BLOCK_SIZE_IN_BYTES, count - posn);
|
||||
|
||||
// Update counter
|
||||
BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++);
|
||||
|
||||
// Encrypt counter to get key stream
|
||||
_transform.TransformBlock(_counter, 0, BLOCK_SIZE_IN_BYTES, _counterOut, 0);
|
||||
|
||||
// XOR with plaintext
|
||||
for (var i = 0; i < blockSize; i++)
|
||||
{
|
||||
result[posn + i] = (byte)(_counterOut[i] ^ buffer[offset + posn + i]);
|
||||
}
|
||||
|
||||
posn += blockSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Finalize HMAC and write authentication code
|
||||
_hmac.TransformFinalBlock([], 0, 0);
|
||||
var authCode = _hmac.Hash!;
|
||||
_stream.Write(authCode, 0, AUTH_CODE_LENGTH);
|
||||
|
||||
_transform.Dispose();
|
||||
_cipher.Dispose();
|
||||
_hmac.Dispose();
|
||||
_stream.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the overhead bytes added by encryption (salt + password verification + auth code).
|
||||
/// </summary>
|
||||
internal static int GetEncryptionOverhead(WinzipAesKeySize keySize) =>
|
||||
GetSaltLength(keySize) + 2 + AUTH_CODE_LENGTH;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the encryption method to use when creating encrypted ZIP archives.
|
||||
/// </summary>
|
||||
public enum ZipEncryptionType
|
||||
{
|
||||
/// <summary>
|
||||
/// No encryption.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// PKWARE Traditional (ZipCrypto) encryption.
|
||||
/// This is the older, less secure encryption method but is widely compatible.
|
||||
/// </summary>
|
||||
PkwareTraditional = 1,
|
||||
|
||||
/// <summary>
|
||||
/// WinZip AES-256 encryption.
|
||||
/// This is the more secure encryption method using AES-256.
|
||||
/// </summary>
|
||||
Aes256 = 2,
|
||||
|
||||
/// <summary>
|
||||
/// WinZip AES-128 encryption.
|
||||
/// This uses AES-128 for encryption.
|
||||
/// </summary>
|
||||
Aes128 = 3,
|
||||
}
|
||||
@@ -21,12 +21,12 @@ internal class ZipHeaderFactory
|
||||
protected LocalEntryHeader? _lastEntryHeader;
|
||||
private readonly string? _password;
|
||||
private readonly StreamingMode _mode;
|
||||
private readonly IArchiveEncoding _archiveEncoding;
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
|
||||
protected ZipHeaderFactory(
|
||||
StreamingMode mode,
|
||||
string? password,
|
||||
IArchiveEncoding archiveEncoding
|
||||
ArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
_mode = mode;
|
||||
@@ -157,8 +157,8 @@ internal class ZipHeaderFactory
|
||||
|
||||
var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2];
|
||||
var passwordVerifyValue = new byte[2];
|
||||
stream.ReadFully(salt);
|
||||
stream.ReadFully(passwordVerifyValue);
|
||||
stream.Read(salt, 0, salt.Length);
|
||||
stream.Read(passwordVerifyValue, 0, 2);
|
||||
entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData(
|
||||
keySize,
|
||||
salt,
|
||||
|
||||
@@ -30,6 +30,7 @@ public sealed class BZip2Stream : Stream, IStreamStack
|
||||
|
||||
private readonly Stream stream;
|
||||
private bool isDisposed;
|
||||
private readonly bool leaveOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Create a BZip2Stream
|
||||
@@ -37,19 +38,30 @@ public sealed class BZip2Stream : Stream, IStreamStack
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="compressionMode">Compression Mode</param>
|
||||
/// <param name="decompressConcatenated">Decompress Concatenated</param>
|
||||
public BZip2Stream(Stream stream, CompressionMode compressionMode, bool decompressConcatenated)
|
||||
/// <param name="leaveOpen">Leave the stream open after disposing</param>
|
||||
public BZip2Stream(
|
||||
Stream stream,
|
||||
CompressionMode compressionMode,
|
||||
bool decompressConcatenated,
|
||||
bool leaveOpen = false
|
||||
)
|
||||
{
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugConstruct(typeof(BZip2Stream));
|
||||
#endif
|
||||
this.leaveOpen = leaveOpen;
|
||||
Mode = compressionMode;
|
||||
if (Mode == CompressionMode.Compress)
|
||||
{
|
||||
this.stream = new CBZip2OutputStream(stream);
|
||||
this.stream = new CBZip2OutputStream(stream, 9, leaveOpen);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.stream = new CBZip2InputStream(stream, decompressConcatenated);
|
||||
this.stream = new CBZip2InputStream(
|
||||
stream,
|
||||
decompressConcatenated,
|
||||
leaveOpen: leaveOpen
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
private int computedBlockCRC,
|
||||
computedCombinedCRC;
|
||||
private readonly bool decompressConcatenated;
|
||||
private readonly bool leaveOpen;
|
||||
|
||||
private int i2,
|
||||
count,
|
||||
@@ -181,9 +182,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
private char z;
|
||||
private bool isDisposed;
|
||||
|
||||
public CBZip2InputStream(Stream zStream, bool decompressConcatenated)
|
||||
public CBZip2InputStream(Stream zStream, bool decompressConcatenated, bool leaveOpen = false)
|
||||
{
|
||||
this.decompressConcatenated = decompressConcatenated;
|
||||
this.leaveOpen = leaveOpen;
|
||||
ll8 = null;
|
||||
tt = null;
|
||||
BsSetStream(zStream);
|
||||
@@ -207,7 +209,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
this.DebugDispose(typeof(CBZip2InputStream));
|
||||
#endif
|
||||
base.Dispose(disposing);
|
||||
bsStream?.Dispose();
|
||||
if (!leaveOpen)
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static int[][] InitIntArray(int n1, int n2)
|
||||
@@ -398,7 +403,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
|
||||
|
||||
private void BsFinishedWithStream()
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
if (!leaveOpen)
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
}
|
||||
bsStream = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -341,12 +341,14 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
|
||||
|
||||
private int currentChar = -1;
|
||||
private int runLength;
|
||||
private readonly bool leaveOpen;
|
||||
|
||||
public CBZip2OutputStream(Stream inStream)
|
||||
: this(inStream, 9) { }
|
||||
public CBZip2OutputStream(Stream inStream, bool leaveOpen = false)
|
||||
: this(inStream, 9, leaveOpen) { }
|
||||
|
||||
public CBZip2OutputStream(Stream inStream, int inBlockSize)
|
||||
public CBZip2OutputStream(Stream inStream, int inBlockSize, bool leaveOpen = false)
|
||||
{
|
||||
this.leaveOpen = leaveOpen;
|
||||
block = null;
|
||||
quadrant = null;
|
||||
zptr = null;
|
||||
@@ -481,7 +483,10 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
|
||||
this.DebugDispose(typeof(CBZip2OutputStream));
|
||||
#endif
|
||||
Dispose();
|
||||
bsStream?.Dispose();
|
||||
if (!leaveOpen)
|
||||
{
|
||||
bsStream?.Dispose();
|
||||
}
|
||||
bsStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,11 +46,13 @@ public sealed class LZipStream : Stream, IStreamStack
|
||||
|
||||
private long _writeCount;
|
||||
private readonly Stream? _originalStream;
|
||||
private readonly bool _leaveOpen;
|
||||
|
||||
public LZipStream(Stream stream, CompressionMode mode)
|
||||
public LZipStream(Stream stream, CompressionMode mode, bool leaveOpen = false)
|
||||
{
|
||||
Mode = mode;
|
||||
_originalStream = stream;
|
||||
_leaveOpen = leaveOpen;
|
||||
|
||||
if (mode == CompressionMode.Decompress)
|
||||
{
|
||||
@@ -60,7 +62,7 @@ public sealed class LZipStream : Stream, IStreamStack
|
||||
throw new InvalidFormatException("Not an LZip stream");
|
||||
}
|
||||
var properties = GetProperties(dSize);
|
||||
_stream = new LzmaStream(properties, stream);
|
||||
_stream = new LzmaStream(properties, stream, leaveOpen: leaveOpen);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -127,7 +129,7 @@ public sealed class LZipStream : Stream, IStreamStack
|
||||
{
|
||||
Finish();
|
||||
_stream.Dispose();
|
||||
if (Mode == CompressionMode.Compress)
|
||||
if (Mode == CompressionMode.Compress && !_leaveOpen)
|
||||
{
|
||||
_originalStream?.Dispose();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class LzmaStream : Stream, IStreamStack
|
||||
private readonly Stream _inputStream;
|
||||
private readonly long _inputSize;
|
||||
private readonly long _outputSize;
|
||||
private readonly bool _leaveOpen;
|
||||
|
||||
private readonly int _dictionarySize;
|
||||
private readonly OutWindow _outWindow = new();
|
||||
@@ -56,14 +57,28 @@ public class LzmaStream : Stream, IStreamStack
|
||||
private readonly Encoder _encoder;
|
||||
private bool _isDisposed;
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream)
|
||||
: this(properties, inputStream, -1, -1, null, properties.Length < 5) { }
|
||||
public LzmaStream(byte[] properties, Stream inputStream, bool leaveOpen = false)
|
||||
: this(properties, inputStream, -1, -1, null, properties.Length < 5, leaveOpen) { }
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream, long inputSize)
|
||||
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5) { }
|
||||
public LzmaStream(byte[] properties, Stream inputStream, long inputSize, bool leaveOpen = false)
|
||||
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5, leaveOpen) { }
|
||||
|
||||
public LzmaStream(byte[] properties, Stream inputStream, long inputSize, long outputSize)
|
||||
: this(properties, inputStream, inputSize, outputSize, null, properties.Length < 5) { }
|
||||
public LzmaStream(
|
||||
byte[] properties,
|
||||
Stream inputStream,
|
||||
long inputSize,
|
||||
long outputSize,
|
||||
bool leaveOpen = false
|
||||
)
|
||||
: this(
|
||||
properties,
|
||||
inputStream,
|
||||
inputSize,
|
||||
outputSize,
|
||||
null,
|
||||
properties.Length < 5,
|
||||
leaveOpen
|
||||
) { }
|
||||
|
||||
public LzmaStream(
|
||||
byte[] properties,
|
||||
@@ -71,13 +86,15 @@ public class LzmaStream : Stream, IStreamStack
|
||||
long inputSize,
|
||||
long outputSize,
|
||||
Stream presetDictionary,
|
||||
bool isLzma2
|
||||
bool isLzma2,
|
||||
bool leaveOpen = false
|
||||
)
|
||||
{
|
||||
_inputStream = inputStream;
|
||||
_inputSize = inputSize;
|
||||
_outputSize = outputSize;
|
||||
_isLzma2 = isLzma2;
|
||||
_leaveOpen = leaveOpen;
|
||||
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugConstruct(typeof(LzmaStream));
|
||||
@@ -179,7 +196,10 @@ public class LzmaStream : Stream, IStreamStack
|
||||
{
|
||||
_position = _encoder.Code(null, true);
|
||||
}
|
||||
_inputStream?.Dispose();
|
||||
if (!_leaveOpen)
|
||||
{
|
||||
_inputStream?.Dispose();
|
||||
}
|
||||
_outWindow.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace SharpCompress.Readers.Ace
|
||||
/// </remarks>
|
||||
public abstract class AceReader : AbstractReader<AceEntry, AceVolume>
|
||||
{
|
||||
private readonly IArchiveEncoding _archiveEncoding;
|
||||
private readonly ArchiveEncoding _archiveEncoding;
|
||||
|
||||
internal AceReader(ReaderOptions options)
|
||||
: base(options, ArchiveType.Ace)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
<Authors>Adam Hathcock</Authors>
|
||||
<TargetFrameworks>net48;netstandard20;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' Or '$(TargetFramework)' == 'netstandard20' ">
|
||||
<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;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ internal class ZipCentralDirectoryEntry
|
||||
{
|
||||
private readonly ZipCompressionMethod compression;
|
||||
private readonly string fileName;
|
||||
private readonly IArchiveEncoding archiveEncoding;
|
||||
private readonly ArchiveEncoding archiveEncoding;
|
||||
|
||||
public ZipCentralDirectoryEntry(
|
||||
ZipCompressionMethod compression,
|
||||
string fileName,
|
||||
ulong headerOffset,
|
||||
IArchiveEncoding archiveEncoding
|
||||
ArchiveEncoding archiveEncoding
|
||||
)
|
||||
{
|
||||
this.compression = compression;
|
||||
@@ -35,16 +35,6 @@ internal class ZipCentralDirectoryEntry
|
||||
internal ushort Zip64HeaderOffset { get; set; }
|
||||
internal ulong HeaderOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The encryption type used for this entry.
|
||||
/// </summary>
|
||||
internal ZipEncryptionType EncryptionType { get; set; } = ZipEncryptionType.None;
|
||||
|
||||
/// <summary>
|
||||
/// The actual compression method (used when compression is WinzipAes).
|
||||
/// </summary>
|
||||
internal ZipCompressionMethod ActualCompression { get; set; }
|
||||
|
||||
internal uint Write(Stream outputStream)
|
||||
{
|
||||
var encodedFilename = archiveEncoding.Encode(fileName);
|
||||
@@ -59,29 +49,12 @@ internal class ZipCentralDirectoryEntry
|
||||
var headeroffsetvalue = zip64 ? uint.MaxValue : (uint)HeaderOffset;
|
||||
var extralength = zip64 ? (2 + 2 + 8 + 8 + 8 + 4) : 0;
|
||||
|
||||
// Add AES extra field length if encrypted with AES
|
||||
if (
|
||||
EncryptionType == ZipEncryptionType.Aes128
|
||||
|| EncryptionType == ZipEncryptionType.Aes256
|
||||
)
|
||||
{
|
||||
extralength += 2 + 2 + 7; // ID + size + data
|
||||
}
|
||||
|
||||
// Determine version needed to extract:
|
||||
// - Version 63 for LZMA, PPMd, BZip2, ZStandard (advanced compression methods)
|
||||
// - Version 51 for WinZip AES encryption
|
||||
// - Version 45 for Zip64 extensions (when Zip64HeaderOffset != 0 or actual sizes require it)
|
||||
// - Version 20 for standard Deflate/None compression
|
||||
byte version;
|
||||
if (
|
||||
EncryptionType == ZipEncryptionType.Aes128
|
||||
|| EncryptionType == ZipEncryptionType.Aes256
|
||||
)
|
||||
{
|
||||
version = 51;
|
||||
}
|
||||
else if (
|
||||
compression == ZipCompressionMethod.LZMA
|
||||
|| compression == ZipCompressionMethod.PPMd
|
||||
|| compression == ZipCompressionMethod.BZip2
|
||||
@@ -102,13 +75,6 @@ internal class ZipCentralDirectoryEntry
|
||||
var flags = Equals(archiveEncoding.GetEncoding(), Encoding.UTF8)
|
||||
? HeaderFlags.Efs
|
||||
: HeaderFlags.None;
|
||||
|
||||
// Add encryption flag
|
||||
if (EncryptionType != ZipEncryptionType.None)
|
||||
{
|
||||
flags |= HeaderFlags.Encrypted;
|
||||
}
|
||||
|
||||
if (!outputStream.CanSeek)
|
||||
{
|
||||
// Cannot use data descriptors with zip64:
|
||||
@@ -128,8 +94,8 @@ internal class ZipCentralDirectoryEntry
|
||||
}
|
||||
}
|
||||
|
||||
// Support for zero byte files (but not for encrypted files which always have encryption overhead)
|
||||
if (Decompressed == 0 && Compressed == 0 && EncryptionType == ZipEncryptionType.None)
|
||||
// Support for zero byte files
|
||||
if (Decompressed == 0 && Compressed == 0)
|
||||
{
|
||||
usedCompression = ZipCompressionMethod.None;
|
||||
}
|
||||
@@ -187,13 +153,11 @@ internal class ZipCentralDirectoryEntry
|
||||
outputStream.Write(intBuf.Slice(0, 4)); // Offset of header
|
||||
|
||||
outputStream.Write(encodedFilename, 0, encodedFilename.Length);
|
||||
|
||||
// Write Zip64 extra field
|
||||
if (zip64)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, 0x0001);
|
||||
outputStream.Write(intBuf.Slice(0, 2));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)(2 + 2 + 8 + 8 + 8 + 4 - 4));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)(extralength - 4));
|
||||
outputStream.Write(intBuf.Slice(0, 2));
|
||||
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(intBuf, Decompressed);
|
||||
@@ -206,28 +170,6 @@ internal class ZipCentralDirectoryEntry
|
||||
outputStream.Write(intBuf.Slice(0, 4)); // VolumeNumber = 0
|
||||
}
|
||||
|
||||
// Write WinZip AES extra field
|
||||
if (
|
||||
EncryptionType == ZipEncryptionType.Aes128
|
||||
|| EncryptionType == ZipEncryptionType.Aes256
|
||||
)
|
||||
{
|
||||
Span<byte> aesExtra = stackalloc byte[11];
|
||||
// Extra field ID: 0x9901 (WinZip AES)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra, 0x9901);
|
||||
// Extra field data size: 7 bytes
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(2), 7);
|
||||
// AES encryption version: 2 (AE-2)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(4), 0x0002);
|
||||
// Vendor ID: "AE" = 0x4541
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(6), 0x4541);
|
||||
// AES encryption strength: 1=128-bit, 3=256-bit
|
||||
aesExtra[8] = EncryptionType == ZipEncryptionType.Aes128 ? (byte)1 : (byte)3;
|
||||
// Actual compression method
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(9), (ushort)ActualCompression);
|
||||
outputStream.Write(aesExtra);
|
||||
}
|
||||
|
||||
outputStream.Write(encodedComment, 0, encodedComment.Length);
|
||||
|
||||
return (uint)(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,8 +28,6 @@ public class ZipWriter : AbstractWriter
|
||||
private long streamPosition;
|
||||
private PpmdProperties? ppmdProps;
|
||||
private readonly bool isZip64;
|
||||
private readonly string? password;
|
||||
private readonly ZipEncryptionType encryptionType;
|
||||
|
||||
public ZipWriter(Stream destination, ZipWriterOptions zipWriterOptions)
|
||||
: base(ArchiveType.Zip, zipWriterOptions)
|
||||
@@ -43,21 +42,6 @@ public class ZipWriter : AbstractWriter
|
||||
compressionType = zipWriterOptions.CompressionType;
|
||||
compressionLevel = zipWriterOptions.CompressionLevel;
|
||||
|
||||
// Initialize encryption settings
|
||||
password = zipWriterOptions.Password;
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
// If password is set but encryption type is None, default to AES-256
|
||||
encryptionType =
|
||||
zipWriterOptions.EncryptionType == ZipEncryptionType.None
|
||||
? ZipEncryptionType.Aes256
|
||||
: zipWriterOptions.EncryptionType;
|
||||
}
|
||||
else
|
||||
{
|
||||
encryptionType = ZipEncryptionType.None;
|
||||
}
|
||||
|
||||
if (WriterOptions.LeaveStreamOpen)
|
||||
{
|
||||
destination = SharpCompressStream.Create(destination, leaveOpen: true);
|
||||
@@ -104,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)
|
||||
@@ -114,20 +98,8 @@ public class ZipWriter : AbstractWriter
|
||||
entryPath = NormalizeFilename(entryPath);
|
||||
options.ModificationDateTime ??= DateTime.Now;
|
||||
options.EntryComment ??= string.Empty;
|
||||
|
||||
// Determine the effective encryption type for this entry
|
||||
var effectiveEncryption = encryptionType;
|
||||
|
||||
// For WinZip AES, the compression method in the header is set to WinzipAes,
|
||||
// and the actual compression method is stored in the extra field
|
||||
var headerCompression =
|
||||
effectiveEncryption == ZipEncryptionType.Aes128
|
||||
|| effectiveEncryption == ZipEncryptionType.Aes256
|
||||
? ZipCompressionMethod.WinzipAes
|
||||
: compression;
|
||||
|
||||
var entry = new ZipCentralDirectoryEntry(
|
||||
headerCompression,
|
||||
compression,
|
||||
entryPath,
|
||||
(ulong)streamPosition,
|
||||
WriterOptions.ArchiveEncoding
|
||||
@@ -135,8 +107,6 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
Comment = options.EntryComment,
|
||||
ModificationTime = options.ModificationDateTime,
|
||||
EncryptionType = effectiveEncryption,
|
||||
ActualCompression = compression,
|
||||
};
|
||||
|
||||
// Use the archive default setting for zip64 and allow overrides
|
||||
@@ -146,23 +116,14 @@ public class ZipWriter : AbstractWriter
|
||||
useZip64 = options.EnableZip64.Value;
|
||||
}
|
||||
|
||||
var headersize = (uint)WriteHeader(
|
||||
entryPath,
|
||||
options,
|
||||
entry,
|
||||
useZip64,
|
||||
effectiveEncryption,
|
||||
compression
|
||||
);
|
||||
var headersize = (uint)WriteHeader(entryPath, options, entry, useZip64);
|
||||
streamPosition += headersize;
|
||||
return new ZipWritingStream(
|
||||
this,
|
||||
OutputStream.NotNull(),
|
||||
entry,
|
||||
compression,
|
||||
options.CompressionLevel ?? compressionLevel,
|
||||
effectiveEncryption,
|
||||
password
|
||||
options.CompressionLevel ?? compressionLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,15 +202,7 @@ public class ZipWriter : AbstractWriter
|
||||
useZip64 = options.EnableZip64.Value;
|
||||
}
|
||||
|
||||
// Directory entries are never encrypted
|
||||
var headersize = (uint)WriteHeader(
|
||||
directoryPath,
|
||||
options,
|
||||
entry,
|
||||
useZip64,
|
||||
ZipEncryptionType.None,
|
||||
compression
|
||||
);
|
||||
var headersize = (uint)WriteHeader(directoryPath, options, entry, useZip64);
|
||||
streamPosition += headersize;
|
||||
entries.Add(entry);
|
||||
}
|
||||
@@ -258,9 +211,7 @@ public class ZipWriter : AbstractWriter
|
||||
string filename,
|
||||
ZipWriterEntryOptions zipWriterEntryOptions,
|
||||
ZipCentralDirectoryEntry entry,
|
||||
bool useZip64,
|
||||
ZipEncryptionType encryption,
|
||||
ZipCompressionMethod actualCompression
|
||||
bool useZip64
|
||||
)
|
||||
{
|
||||
// We err on the side of caution until the zip specification clarifies how to support this
|
||||
@@ -271,30 +222,15 @@ public class ZipWriter : AbstractWriter
|
||||
);
|
||||
}
|
||||
|
||||
// Encryption is only supported with seekable streams for now
|
||||
if (!OutputStream.CanSeek && encryption != ZipEncryptionType.None)
|
||||
{
|
||||
throw new NotSupportedException("Encryption is not supported on non-seekable streams");
|
||||
}
|
||||
|
||||
// Determine the compression method to write in the header
|
||||
var headerCompression =
|
||||
encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256
|
||||
? ZipCompressionMethod.WinzipAes
|
||||
: actualCompression;
|
||||
|
||||
var explicitZipCompressionInfo = ToZipCompressionMethod(
|
||||
zipWriterEntryOptions.CompressionType ?? compressionType
|
||||
);
|
||||
var encodedFilename = WriterOptions.ArchiveEncoding.Encode(filename);
|
||||
|
||||
Span<byte> intBuf = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(intBuf, ZipHeaderFactory.ENTRY_HEADER_BYTES);
|
||||
OutputStream.Write(intBuf);
|
||||
|
||||
// Determine version needed
|
||||
if (encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256)
|
||||
{
|
||||
OutputStream.Write(stackalloc byte[] { 51, 0 }); // WinZip AES requires version 5.1
|
||||
}
|
||||
else if (actualCompression == ZipCompressionMethod.Deflate)
|
||||
if (explicitZipCompressionInfo == ZipCompressionMethod.Deflate)
|
||||
{
|
||||
if (OutputStream.CanSeek && useZip64)
|
||||
{
|
||||
@@ -309,22 +245,14 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
OutputStream.Write(stackalloc byte[] { 63, 0 }); //version says we used PPMd or LZMA
|
||||
}
|
||||
|
||||
var flags = Equals(WriterOptions.ArchiveEncoding.GetEncoding(), Encoding.UTF8)
|
||||
? HeaderFlags.Efs
|
||||
: HeaderFlags.None;
|
||||
|
||||
// Add encryption flag
|
||||
if (encryption != ZipEncryptionType.None)
|
||||
{
|
||||
flags |= HeaderFlags.Encrypted;
|
||||
}
|
||||
|
||||
: 0;
|
||||
if (!OutputStream.CanSeek)
|
||||
{
|
||||
flags |= HeaderFlags.UsePostDataDescriptor;
|
||||
|
||||
if (actualCompression == ZipCompressionMethod.LZMA)
|
||||
if (explicitZipCompressionInfo == ZipCompressionMethod.LZMA)
|
||||
{
|
||||
flags |= HeaderFlags.Bit1; // eos marker
|
||||
}
|
||||
@@ -332,7 +260,7 @@ public class ZipWriter : AbstractWriter
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)flags);
|
||||
OutputStream.Write(intBuf.Slice(0, 2));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)headerCompression);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)explicitZipCompressionInfo);
|
||||
OutputStream.Write(intBuf.Slice(0, 2)); // zipping method
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
intBuf,
|
||||
@@ -347,49 +275,22 @@ public class ZipWriter : AbstractWriter
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)encodedFilename.Length);
|
||||
OutputStream.Write(intBuf.Slice(0, 2)); // filename length
|
||||
|
||||
// Calculate extra field length
|
||||
var extralength = 0;
|
||||
if (OutputStream.CanSeek && useZip64)
|
||||
{
|
||||
extralength += 2 + 2 + 8 + 8; // Zip64 extra field
|
||||
}
|
||||
|
||||
// WinZip AES extra field: 2 (id) + 2 (size) + 2 (version) + 2 (vendor) + 1 (strength) + 2 (actual compression)
|
||||
if (encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256)
|
||||
{
|
||||
extralength += 2 + 2 + 7;
|
||||
extralength = 2 + 2 + 8 + 8;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(intBuf, (ushort)extralength);
|
||||
OutputStream.Write(intBuf.Slice(0, 2)); // extra length
|
||||
OutputStream.Write(encodedFilename, 0, encodedFilename.Length);
|
||||
|
||||
// Write Zip64 extra field
|
||||
if (OutputStream.CanSeek && useZip64)
|
||||
if (extralength != 0)
|
||||
{
|
||||
OutputStream.Write(new byte[2 + 2 + 8 + 8], 0, 2 + 2 + 8 + 8); // reserve space for zip64 data
|
||||
OutputStream.Write(new byte[extralength], 0, extralength); // reserve space for zip64 data
|
||||
entry.Zip64HeaderOffset = (ushort)(6 + 2 + 2 + 4 + 12 + 2 + 2 + encodedFilename.Length);
|
||||
}
|
||||
|
||||
// Write WinZip AES extra field
|
||||
if (encryption == ZipEncryptionType.Aes128 || encryption == ZipEncryptionType.Aes256)
|
||||
{
|
||||
Span<byte> aesExtra = stackalloc byte[11];
|
||||
// Extra field ID: 0x9901 (WinZip AES)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra, 0x9901);
|
||||
// Extra field data size: 7 bytes
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(2), 7);
|
||||
// AES encryption version: 2 (AE-2)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(4), 0x0002);
|
||||
// Vendor ID: "AE" = 0x4541
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(6), 0x4541);
|
||||
// AES encryption strength: 1=128-bit, 3=256-bit
|
||||
aesExtra[8] = encryption == ZipEncryptionType.Aes128 ? (byte)1 : (byte)3;
|
||||
// Actual compression method
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(aesExtra.Slice(9), (ushort)actualCompression);
|
||||
OutputStream.Write(aesExtra);
|
||||
}
|
||||
|
||||
return 6 + 2 + 2 + 4 + 12 + 2 + 2 + encodedFilename.Length + extralength;
|
||||
}
|
||||
|
||||
@@ -485,10 +386,7 @@ public class ZipWriter : AbstractWriter
|
||||
private readonly ZipWriter writer;
|
||||
private readonly ZipCompressionMethod zipCompressionMethod;
|
||||
private readonly int compressionLevel;
|
||||
private readonly ZipEncryptionType encryptionType;
|
||||
private readonly string? password;
|
||||
private SharpCompressStream? counting;
|
||||
private Stream? encryptionStream;
|
||||
private ulong decompressed;
|
||||
|
||||
// Flag to prevent throwing exceptions on Dispose
|
||||
@@ -500,18 +398,15 @@ public class ZipWriter : AbstractWriter
|
||||
Stream originalStream,
|
||||
ZipCentralDirectoryEntry entry,
|
||||
ZipCompressionMethod zipCompressionMethod,
|
||||
int compressionLevel,
|
||||
ZipEncryptionType encryptionType,
|
||||
string? password
|
||||
int compressionLevel
|
||||
)
|
||||
{
|
||||
this.writer = writer;
|
||||
this.originalStream = originalStream;
|
||||
this.writer = writer;
|
||||
this.entry = entry;
|
||||
this.zipCompressionMethod = zipCompressionMethod;
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.encryptionType = encryptionType;
|
||||
this.password = password;
|
||||
writeStream = GetWriteStream(originalStream);
|
||||
}
|
||||
|
||||
@@ -533,47 +428,6 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
counting = new SharpCompressStream(writeStream, leaveOpen: true);
|
||||
Stream output = counting;
|
||||
|
||||
// Wrap with encryption stream if needed
|
||||
if (encryptionType == ZipEncryptionType.Aes128)
|
||||
{
|
||||
encryptionStream = new WinzipAesEncryptionStream(
|
||||
counting,
|
||||
password!,
|
||||
WinzipAesKeySize.KeySize128
|
||||
);
|
||||
output = encryptionStream;
|
||||
}
|
||||
else if (encryptionType == ZipEncryptionType.Aes256)
|
||||
{
|
||||
encryptionStream = new WinzipAesEncryptionStream(
|
||||
counting,
|
||||
password!,
|
||||
WinzipAesKeySize.KeySize256
|
||||
);
|
||||
output = encryptionStream;
|
||||
}
|
||||
else if (encryptionType == ZipEncryptionType.PkwareTraditional)
|
||||
{
|
||||
// For PKWARE traditional encryption, we need to write the encryption header
|
||||
// and wrap the stream in the crypto stream
|
||||
var encryptor = PkwareTraditionalEncryptionData.ForWrite(
|
||||
password!,
|
||||
writer.WriterOptions.ArchiveEncoding
|
||||
);
|
||||
// Write the encryption header (12 bytes)
|
||||
// CRC is not known yet, so we use 0 for now (it gets verified with time for streaming)
|
||||
var header = encryptor.GenerateEncryptionHeader(0, 0);
|
||||
counting.Write(header, 0, header.Length);
|
||||
|
||||
encryptionStream = new PkwareTraditionalCryptoStream(
|
||||
new NonDisposingStream(counting),
|
||||
encryptor,
|
||||
CryptoMode.Encrypt
|
||||
);
|
||||
output = encryptionStream;
|
||||
}
|
||||
|
||||
switch (zipCompressionMethod)
|
||||
{
|
||||
case ZipCompressionMethod.None:
|
||||
@@ -583,24 +437,17 @@ public class ZipWriter : AbstractWriter
|
||||
case ZipCompressionMethod.Deflate:
|
||||
{
|
||||
return new DeflateStream(
|
||||
output,
|
||||
counting,
|
||||
CompressionMode.Compress,
|
||||
(CompressionLevel)compressionLevel
|
||||
);
|
||||
}
|
||||
case ZipCompressionMethod.BZip2:
|
||||
{
|
||||
return new BZip2Stream(output, CompressionMode.Compress, false);
|
||||
return new BZip2Stream(counting, CompressionMode.Compress, false);
|
||||
}
|
||||
case ZipCompressionMethod.LZMA:
|
||||
{
|
||||
// LZMA with encryption is not supported per ZIP spec
|
||||
if (encryptionType != ZipEncryptionType.None)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"LZMA compression with encryption is not supported"
|
||||
);
|
||||
}
|
||||
counting.WriteByte(9);
|
||||
counting.WriteByte(20);
|
||||
counting.WriteByte(5);
|
||||
@@ -609,7 +456,7 @@ public class ZipWriter : AbstractWriter
|
||||
var lzmaStream = new LzmaStream(
|
||||
new LzmaEncoderProperties(!originalStream.CanSeek),
|
||||
false,
|
||||
output
|
||||
counting
|
||||
);
|
||||
counting.Write(lzmaStream.Properties, 0, lzmaStream.Properties.Length);
|
||||
return lzmaStream;
|
||||
@@ -617,11 +464,11 @@ public class ZipWriter : AbstractWriter
|
||||
case ZipCompressionMethod.PPMd:
|
||||
{
|
||||
counting.Write(writer.PpmdProperties.Properties, 0, 2);
|
||||
return new PpmdStream(writer.PpmdProperties, output, true);
|
||||
return new PpmdStream(writer.PpmdProperties, counting, true);
|
||||
}
|
||||
case ZipCompressionMethod.ZStandard:
|
||||
{
|
||||
return new CompressionStream(output, compressionLevel);
|
||||
return new CompressionStream(counting, compressionLevel);
|
||||
}
|
||||
default:
|
||||
{
|
||||
@@ -644,9 +491,6 @@ public class ZipWriter : AbstractWriter
|
||||
{
|
||||
writeStream.Dispose();
|
||||
|
||||
// Dispose encryption stream to finalize encryption (e.g., write auth code for AES)
|
||||
encryptionStream?.Dispose();
|
||||
|
||||
if (limitsExceeded)
|
||||
{
|
||||
// We have written invalid data into the archive,
|
||||
@@ -668,24 +512,12 @@ public class ZipWriter : AbstractWriter
|
||||
|
||||
if (originalStream.CanSeek)
|
||||
{
|
||||
// Clear UsePostDataDescriptor flag (bit 3) since we're updating sizes in place
|
||||
// But preserve the Encrypted flag (bit 0) if encryption is enabled
|
||||
originalStream.Position = (long)(entry.HeaderOffset + 6);
|
||||
// Only the Encrypted flag should be in the low byte for seekable streams
|
||||
originalStream.WriteByte(
|
||||
encryptionType != ZipEncryptionType.None
|
||||
? (byte)HeaderFlags.Encrypted
|
||||
: (byte)0
|
||||
);
|
||||
originalStream.WriteByte(0);
|
||||
|
||||
if (
|
||||
countingCount == 0
|
||||
&& entry.Decompressed == 0
|
||||
&& encryptionType == ZipEncryptionType.None
|
||||
)
|
||||
if (countingCount == 0 && entry.Decompressed == 0)
|
||||
{
|
||||
// set compression to STORED for zero byte files (no compression data)
|
||||
// But not if encrypted, as encrypted files always have some data
|
||||
originalStream.Position = (long)(entry.HeaderOffset + 8);
|
||||
originalStream.WriteByte(0);
|
||||
originalStream.WriteByte(0);
|
||||
@@ -807,46 +639,5 @@ public class ZipWriter : AbstractWriter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stream wrapper that doesn't dispose the underlying stream when disposed.
|
||||
/// This is used in encryption scenarios where the crypto stream would otherwise
|
||||
/// dispose the counting stream prematurely, before we can read the final count.
|
||||
/// </summary>
|
||||
private class NonDisposingStream : Stream
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
|
||||
public NonDisposingStream(Stream stream) => _stream = stream;
|
||||
|
||||
public override bool CanRead => _stream.CanRead;
|
||||
public override bool CanSeek => _stream.CanSeek;
|
||||
public override bool CanWrite => _stream.CanWrite;
|
||||
public override long Length => _stream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _stream.Position;
|
||||
set => _stream.Position = value;
|
||||
}
|
||||
|
||||
public override void Flush() => _stream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
_stream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _stream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
_stream.Write(buffer, offset, count);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
// Don't dispose the underlying stream
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Nested type: ZipWritingStream
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using D = SharpCompress.Compressors.Deflate;
|
||||
|
||||
@@ -25,8 +24,6 @@ public class ZipWriterOptions : WriterOptions
|
||||
{
|
||||
UseZip64 = writerOptions.UseZip64;
|
||||
ArchiveComment = writerOptions.ArchiveComment;
|
||||
Password = writerOptions.Password;
|
||||
EncryptionType = writerOptions.EncryptionType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,19 +80,4 @@ public class ZipWriterOptions : WriterOptions
|
||||
/// are less than 4GiB in length.
|
||||
/// </summary>
|
||||
public bool UseZip64 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password to use for encrypting the ZIP archive entries.
|
||||
/// When set, entries will be encrypted using the specified <see cref="EncryptionType"/>.
|
||||
/// If null or empty, no encryption is applied.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The encryption type to use when a password is set.
|
||||
/// Defaults to <see cref="ZipEncryptionType.None"/>.
|
||||
/// When <see cref="Password"/> is set and this is <see cref="ZipEncryptionType.None"/>,
|
||||
/// <see cref="ZipEncryptionType.Aes256"/> will be used automatically.
|
||||
/// </summary>
|
||||
public ZipEncryptionType EncryptionType { get; set; } = ZipEncryptionType.None;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -118,12 +118,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"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
@@ -164,8 +164,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.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
@@ -179,8 +179,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",
|
||||
@@ -204,57 +204,85 @@
|
||||
"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",
|
||||
"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=="
|
||||
}
|
||||
},
|
||||
"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>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Compressors.Lzw;
|
||||
@@ -152,9 +153,21 @@ public class DisposalTests
|
||||
[Fact]
|
||||
public void LZipStream_Disposal()
|
||||
{
|
||||
// LZipStream always disposes inner stream
|
||||
// LZipStream now supports leaveOpen parameter
|
||||
// Use Compress mode to avoid need for valid input header
|
||||
VerifyAlwaysDispose(stream => new LZipStream(stream, CompressionMode.Compress));
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) => new LZipStream(stream, CompressionMode.Compress, leaveOpen)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Disposal()
|
||||
{
|
||||
// BZip2Stream now supports leaveOpen parameter
|
||||
VerifyStreamDisposal(
|
||||
(stream, leaveOpen) =>
|
||||
new BZip2Stream(stream, CompressionMode.Compress, false, leaveOpen)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
226
tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs
Normal file
226
tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Streams;
|
||||
|
||||
public class LeaveOpenBehaviorTests
|
||||
{
|
||||
private static byte[] CreateTestData() =>
|
||||
Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Compress_LeaveOpen_False()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Compress,
|
||||
false,
|
||||
leaveOpen: false
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Compress_LeaveOpen_True()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
byte[] compressed;
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Compress,
|
||||
false,
|
||||
leaveOpen: true
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
|
||||
// Should be able to read the compressed data
|
||||
innerStream.Position = 0;
|
||||
compressed = new byte[innerStream.Length];
|
||||
innerStream.Read(compressed, 0, compressed.Length);
|
||||
Assert.True(compressed.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Decompress_LeaveOpen_False()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var bzip2 = new BZip2Stream(memStream, CompressionMode.Compress, false, true))
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Decompress,
|
||||
false,
|
||||
leaveOpen: false
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BZip2Stream_Decompress_LeaveOpen_True()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var bzip2 = new BZip2Stream(memStream, CompressionMode.Compress, false, true))
|
||||
{
|
||||
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
bzip2.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (
|
||||
var bzip2 = new BZip2Stream(
|
||||
innerStream,
|
||||
CompressionMode.Decompress,
|
||||
false,
|
||||
leaveOpen: true
|
||||
)
|
||||
)
|
||||
{
|
||||
bzip2.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
|
||||
// Should still be able to use the stream
|
||||
innerStream.Position = 0;
|
||||
Assert.True(innerStream.CanRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Compress_LeaveOpen_False()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Compress, leaveOpen: false))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Compress_LeaveOpen_True()
|
||||
{
|
||||
using var innerStream = new TestStream(new MemoryStream());
|
||||
byte[] compressed;
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Compress, leaveOpen: true))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
|
||||
// Should be able to read the compressed data
|
||||
innerStream.Position = 0;
|
||||
compressed = new byte[innerStream.Length];
|
||||
innerStream.Read(compressed, 0, compressed.Length);
|
||||
Assert.True(compressed.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Decompress_LeaveOpen_False()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var lzip = new LZipStream(memStream, CompressionMode.Compress, true))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Decompress, leaveOpen: false))
|
||||
{
|
||||
lzip.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LZipStream_Decompress_LeaveOpen_True()
|
||||
{
|
||||
// First compress some data
|
||||
var memStream = new MemoryStream();
|
||||
using (var lzip = new LZipStream(memStream, CompressionMode.Compress, true))
|
||||
{
|
||||
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
|
||||
lzip.Finish();
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
using var innerStream = new TestStream(memStream);
|
||||
var decompressed = new byte[CreateTestData().Length];
|
||||
|
||||
using (var lzip = new LZipStream(innerStream, CompressionMode.Decompress, leaveOpen: true))
|
||||
{
|
||||
lzip.Read(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
Assert.False(
|
||||
innerStream.IsDisposed,
|
||||
"Inner stream should NOT be disposed when leaveOpen=true"
|
||||
);
|
||||
Assert.Equal(CreateTestData(), decompressed);
|
||||
|
||||
// Should still be able to use the stream
|
||||
innerStream.Position = 0;
|
||||
Assert.True(innerStream.CanRead);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
@@ -102,208 +97,4 @@ public class ZipWriterTests : WriterTests
|
||||
Assert.Throws<InvalidFormatException>(() =>
|
||||
Write(CompressionType.Rar, "Zip.ppmd.noEmptyDirs.zip", "Zip.ppmd.noEmptyDirs.zip")
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public void Zip_Deflate_Encrypted_Aes256_WriteAndRead()
|
||||
{
|
||||
const string password = "test_password";
|
||||
const string testContent = "Hello, this is a test file for encrypted ZIP.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes256,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(testContent);
|
||||
writer.Write("test.txt", new MemoryStream(contentBytes));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
Assert.Equal("test.txt", entry.Key);
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Deflate_Encrypted_Aes128_WriteAndRead()
|
||||
{
|
||||
const string password = "test_password";
|
||||
const string testContent = "Hello, this is a test file for encrypted ZIP with AES-128.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes128,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(testContent);
|
||||
writer.Write("test.txt", new MemoryStream(contentBytes));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
Assert.Equal("test.txt", entry.Key);
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_None_Encrypted_Aes256_WriteAndRead()
|
||||
{
|
||||
const string password = "test_password";
|
||||
const string testContent = "Uncompressed but encrypted content.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP with no compression
|
||||
var options = new ZipWriterOptions(CompressionType.None)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes256,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(testContent);
|
||||
writer.Write("uncompressed.txt", new MemoryStream(contentBytes));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
Assert.Equal("uncompressed.txt", entry.Key);
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Encrypted_MultipleFiles_WriteAndRead()
|
||||
{
|
||||
const string password = "multi_file_password";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write encrypted ZIP with multiple files
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
EncryptionType = ZipEncryptionType.Aes256,
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
writer.Write(
|
||||
"file1.txt",
|
||||
new MemoryStream(Encoding.UTF8.GetBytes("Content of file 1"))
|
||||
);
|
||||
writer.Write(
|
||||
"file2.txt",
|
||||
new MemoryStream(Encoding.UTF8.GetBytes("Content of file 2"))
|
||||
);
|
||||
writer.Write(
|
||||
"folder/file3.txt",
|
||||
new MemoryStream(Encoding.UTF8.GetBytes("Content of file 3"))
|
||||
);
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
Assert.Contains("Content of file", content);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_Encrypted_DefaultEncryption_WhenPasswordSet()
|
||||
{
|
||||
const string password = "auto_encryption";
|
||||
const string testContent = "Auto encryption type test.";
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// Write ZIP with password but no explicit encryption type
|
||||
// Should default to AES-256
|
||||
var options = new ZipWriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
Password = password,
|
||||
// EncryptionType not set, should default to AES-256 when password is provided
|
||||
};
|
||||
|
||||
using (var writer = new ZipWriter(memoryStream, options))
|
||||
{
|
||||
writer.Write("auto.txt", new MemoryStream(Encoding.UTF8.GetBytes(testContent)));
|
||||
}
|
||||
|
||||
// Read back the encrypted ZIP
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using var archive = ZipArchive.Open(
|
||||
memoryStream,
|
||||
new ReaderOptions { Password = password }
|
||||
);
|
||||
|
||||
var entry = archive.Entries.First(e => !e.IsDirectory);
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
Assert.Equal(testContent, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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