Compare commits

..

55 Commits

Author SHA1 Message Date
Adam Hathcock
22d15f73f0 Merge pull request #1181 from adamhathcock/adam/add-aot
Add AOT to props and clean up in release
2026-02-03 16:55:59 +00:00
Adam Hathcock
4e0d78d6c8 update desc 2026-02-03 16:41:19 +00:00
Adam Hathcock
63a1927838 Merge pull request #1182 from adamhathcock/copilot/sub-pr-1181
[WIP] WIP address feedback on AOT props and cleanup
2026-02-03 16:32:02 +00:00
copilot-swe-agent[bot]
3d745bfa05 Fix invalid TFM: change netstandard20 to netstandard2.0
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-03 16:30:47 +00:00
copilot-swe-agent[bot]
ce26d50792 Initial plan 2026-02-03 16:28:28 +00:00
Adam Hathcock
01e162fcc4 properly add netstandard 2 support 2026-02-03 16:17:58 +00:00
Adam Hathcock
443f7b8b0c Add AOT to props and clean up in release 2026-02-03 16:13:15 +00:00
Adam Hathcock
df63e152c1 Merge pull request #1178 from adamhathcock/copilot/fix-infinite-loop-rar-archive-again
Fix infinite loop in SourceStream.Seek for malformed archives
2026-02-02 10:57:10 +00:00
copilot-swe-agent[bot]
ad7e64ba43 Fix test to use correct RarArchive API - all RAR tests passing
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-02 09:33:37 +00:00
copilot-swe-agent[bot]
8737b7a38e Apply infinite loop fix to SourceStream.cs and add test case
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-02 09:31:38 +00:00
copilot-swe-agent[bot]
13199fcfd1 Initial plan 2026-02-02 09:28:50 +00:00
Adam Hathcock
9a7bdd39e8 Merge pull request #1172 from adamhathcock/copilot/fix-sevenzip-contiguous-streams
Fix SevenZipReader to maintain contiguous stream state for solid archives
2026-01-28 08:35:28 +00:00
Adam Hathcock
484bc740d7 Update src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 08:26:28 +00:00
Adam Hathcock
8a67d501a8 Don't use reflection in tests 2026-01-28 08:10:06 +00:00
copilot-swe-agent[bot]
3c87242bd0 Add test to verify folder stream reuse in solid archives
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 17:29:44 +00:00
copilot-swe-agent[bot]
999124e68e Remove unused _currentFolderIndex field
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 17:03:20 +00:00
copilot-swe-agent[bot]
db2f5c9cb9 Fix SevenZipReader to iterate entries as contiguous streams
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 17:01:18 +00:00
Adam Hathcock
af08a7cd54 Merge pull request #1169 from adamhathcock/copilot/fix-zip-parsing-regression
Fix ZIP parsing failure on non-seekable streams with short reads
2026-01-27 16:54:12 +00:00
copilot-swe-agent[bot]
72eaf66f05 Initial plan 2026-01-27 16:53:53 +00:00
Adam Hathcock
8a3be35d67 Update tests/SharpCompress.Test/Zip/ZipShortReadTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-27 16:43:13 +00:00
copilot-swe-agent[bot]
d59e4c2a0d Refactor FillBuffer to use ReadFully pattern
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 16:25:24 +00:00
copilot-swe-agent[bot]
71655e04c4 Apply code formatting with CSharpier
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 16:02:26 +00:00
copilot-swe-agent[bot]
a706a9d725 Fix ZIP parsing regression with short reads on non-seekable streams
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 16:00:44 +00:00
copilot-swe-agent[bot]
970934a40b Initial plan 2026-01-27 15:51:50 +00:00
Adam Hathcock
a9c28a7b62 Merge pull request #1165 from adamhathcock/adam/buffer-size-consolidation
(Release) Buffer size consolidation
2026-01-27 14:41:14 +00:00
Adam Hathcock
4d31436740 constant should be a static property 2026-01-27 12:39:01 +00:00
Adam Hathcock
c82744c51c fmt 2026-01-27 12:15:31 +00:00
Adam Hathcock
f0eaddc6a6 Merge remote-tracking branch 'origin/adam/buffer-size-consolidation' into adam/buffer-size-consolidation 2026-01-27 12:14:17 +00:00
Adam Hathcock
d6156f0f1e release branch builds increment patch versions and master builds increment minor versions 2026-01-27 12:14:03 +00:00
Adam Hathcock
3c88c7fdd5 Merge pull request #1167 from adamhathcock/copilot/sub-pr-1165-again
Fix grammatical errors in ArcFactory comment documentation
2026-01-27 11:58:25 +00:00
Adam Hathcock
d11f6aefb0 Merge pull request #1166 from adamhathcock/copilot/sub-pr-1165
Add [Obsolete] attribute to ReaderOptions.DefaultBufferSize for backward compatibility
2026-01-27 11:57:54 +00:00
copilot-swe-agent[bot]
010a38bb73 Add clarifying comment about buffer size value difference
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 11:54:50 +00:00
copilot-swe-agent[bot]
53f12d75db Add [Obsolete] attribute to ReaderOptions.DefaultBufferSize
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 11:53:37 +00:00
copilot-swe-agent[bot]
6c866324b2 Fix grammatical errors in ArcFactory comments
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-27 11:49:29 +00:00
copilot-swe-agent[bot]
a114155189 Initial plan 2026-01-27 11:48:05 +00:00
copilot-swe-agent[bot]
014bbc3ea4 Initial plan 2026-01-27 11:47:52 +00:00
Adam Hathcock
d52facd4ab Remove change 2026-01-27 10:48:32 +00:00
Adam Hathcock
0a50386ada Using Constants class differently 2026-01-27 10:46:54 +00:00
Adam Hathcock
b9fc680548 Merge pull request #1160 from adamhathcock/adam/check-if-seek
add check to see if we need to seek before hand
2026-01-26 12:24:39 +00:00
Adam Hathcock
7dcc13c1f0 Merge pull request #1161 from adamhathcock/copilot/sub-pr-1160
Fix ArrayPool corruption from double-disposal in BufferedSubStream
2026-01-26 12:15:55 +00:00
copilot-swe-agent[bot]
56d3091688 Fix condition order to check CanSeek before Position
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-26 12:12:08 +00:00
copilot-swe-agent[bot]
a0af0604d1 Add disposal checks to RefillCache methods
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-26 12:11:16 +00:00
copilot-swe-agent[bot]
875c2d7694 Fix BufferedSubStream double-dispose issue with ArrayPool
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-26 12:10:19 +00:00
Adam Hathcock
8c95f863cb do CanSeek first 2026-01-26 12:06:57 +00:00
copilot-swe-agent[bot]
ddf37e82c2 Initial plan 2026-01-26 12:06:38 +00:00
Adam Hathcock
a82fda98d7 more testing and add pooling to cache 2026-01-26 11:45:25 +00:00
Adam Hathcock
44e4b1804e add check to see if we need to seek before hand 2026-01-26 09:41:13 +00:00
Adam Hathcock
4ca1a7713e Merge pull request #1157 from adamhathcock/adam/1154-release
Merge pull request #1156 from adamhathcock/copilot/fix-sharpcompress-…
2026-01-25 11:36:59 +00:00
Adam Hathcock
9caf7be928 Revert testing 2026-01-24 10:23:02 +00:00
Adam Hathcock
bf4217fde6 Merge pull request #1156 from adamhathcock/copilot/fix-sharpcompress-archive-iteration
Fix silent iteration failure when input stream throws on Flush()
# Conflicts:
#	src/SharpCompress/packages.lock.json
2026-01-24 10:18:02 +00:00
Adam Hathcock
d5a8c37113 Merge pull request #1154 from adamhathcock/adam/1151-release
Adam/1151 release cherry pick
2026-01-23 09:31:03 +00:00
Adam Hathcock
21ce9a38e6 fix up tests 2026-01-23 09:04:55 +00:00
Adam Hathcock
7732fbb698 Merge pull request #1151 from adamhathcock/copilot/fix-entrystream-flush-issue
Fix EntryStream.Dispose() throwing NotSupportedException on non-seekable streams
2026-01-23 08:59:56 +00:00
Adam Hathcock
97879f18b6 Merge pull request #1146 from adamhathcock/adam/pr-1145-release
Merge pull request #1145 from adamhathcock/copilot/add-leaveopen-para…
2026-01-19 10:35:33 +00:00
Adam Hathcock
d74454f7e9 Merge pull request #1145 from adamhathcock/copilot/add-leaveopen-parameter-lzipstream
Add leaveOpen parameter to LZipStream and BZip2Stream
2026-01-19 09:58:10 +00:00
79 changed files with 1514 additions and 1224 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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=="
}
}
}

View File

@@ -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));

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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)
{

View File

@@ -46,7 +46,7 @@ namespace SharpCompress.Common.Ace.Headers
}
}
public AceFileHeader(IArchiveEncoding archiveEncoding)
public AceFileHeader(ArchiveEncoding archiveEncoding)
: base(archiveEncoding, AceHeaderType.FILE) { }
/// <summary>

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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);
}
}

View 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;
}

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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();
}

View File

@@ -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.

View File

@@ -15,7 +15,7 @@ internal class SevenZipFilePart : FilePart
ArchiveDatabase database,
int index,
CFileItem fileEntry,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
: base(archiveEncoding)
{

View File

@@ -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;

View File

@@ -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))

View File

@@ -10,7 +10,7 @@ internal static class TarHeaderFactory
internal static IEnumerable<TarHeader?> ReadHeader(
StreamingMode mode,
Stream stream,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
{
while (true)

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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; }

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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.

View File

@@ -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) =>

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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}."
);
}
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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="\" />

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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)(

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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=="
}
}
}

View File

@@ -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"
}

View File

@@ -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

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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]

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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}");
}
}

View File

@@ -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}");
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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",

Binary file not shown.