diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 4ed271ae2..000000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,17 +0,0 @@
-### Steps to reproduce
-1.
-2.
-3.
-
-### Expected behavior
-Tell us what should happen
-
-### Actual behavior
-Tell us what happens instead
-
-### Version of SharpZipLib
-
-### Obtained from (only keep the relevant lines)
-- Compiled from source, commit: _______
-- Downloaded from GitHub
-- Package installed using NuGet
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 000000000..a1620f07a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,92 @@
+name: 🐛 Bug report
+description: Create a report to help us improve
+labels: ["bug"]
+
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Describe the bug
+ description: A clear and concise description of what the bug is
+ validations:
+ required: true
+
+ - type: input
+ id: reproduce-code
+ attributes:
+ description: |
+ If possible, the best way to display an issue is by making a reproducable code snippet available at jsfiddle.
+ Create a dotnet fiddle which reproduces your issue. You can use [this template](https://p1k.se/sharpziplib-repro) or [create a new one](https://dotnetfiddle.net/).
+ placeholder: https://dotnetfiddle.net/r39r0c0d3
+ label: Reproduction Code
+
+ - type: textarea
+ id: reproduce-steps
+ attributes:
+ label: Steps to reproduce
+ description: Steps to reproduce the behavior
+ placeholder: |
+ 1. Go to '...'
+ 2. Click on '....'
+ 3. Scroll down to '....'
+ 4. See error
+ validations:
+ required: true
+
+ - id: expected
+ type: textarea
+ attributes:
+ label: Expected behavior
+ description: A clear and concise description of what you expected to happen.
+ validations:
+ required: true
+
+ - id: operating-system
+ type: dropdown
+ attributes:
+ label: Operating System
+ multiple: true
+ options:
+ - Windows
+ - macOS
+ - Linux
+ validations:
+ required: false
+
+ - id: framework
+ type: dropdown
+ attributes:
+ label: Framework Version
+ multiple: true
+ options:
+ - .NET 7
+ - .NET 6
+ - .NET 5
+ - .NET Core v3 and earlier
+ - .NET Framework 4.x
+ - Unity
+ - Other
+ validations:
+ required: false
+
+ - id: tags
+ type: dropdown
+ attributes:
+ label: Tags
+ description: What areas are your issue related to?
+ multiple: true
+ options:
+ - ZIP
+ - GZip
+ - Tar
+ - BZip2
+ - Encoding
+ - Encryption
+ - Documentation
+ - Async
+ - Performance
+
+ - type: textarea
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..5a0d4a50e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Ask a question
+ url: https://github.com/icsharpcode/SharpZipLib/discussions/new?category=q-a
+ about: Post any questions in QA discussions instead of creating an issue
+ - name: Discuss
+ url: https://github.com/icsharpcode/SharpZipLib/discussions/new
+ about: Discuss with other community members
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 000000000..5683f0e84
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,52 @@
+name: 💡 Feature request
+description: Have a new idea/feature ? Please suggest!
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Is your feature request related to a problem? Please describe.
+ description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+ validations:
+ required: true
+
+ - type: textarea
+ id: solution
+ attributes:
+ label: Describe the solution you'd like
+ description: A clear and concise description of what you want to happen.
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Describe alternatives you've considered
+ description: A clear and concise description of any alternative solutions or features you've considered.
+ validations:
+ required: true
+
+ - id: tags
+ type: dropdown
+ attributes:
+ label: Tags
+ description: What areas are your feature request related to?
+ multiple: true
+ options:
+ - ZIP
+ - GZip
+ - Tar
+ - BZip2
+ - Encoding
+ - Encryption
+ - Documentation
+ - Async
+ - Performance
+
+ - type: textarea
+ id: extrainfo
+ attributes:
+ label: Additional context
+ description: Add any other context or screenshots about the feature request here.
+ validations:
+ required: false
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 463b5d6fb..a77a0e278 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -23,7 +23,7 @@ jobs:
env:
LIB_PROJ: src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
ref: ${{ github.events.inputs.tag }}
fetch-depth: 0
@@ -79,7 +79,7 @@ jobs:
DOTCOVER_PKG: jetbrains.dotcover.commandlinetools
COVER_SNAPSHOT: SharpZipLib.dcvr
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 549469d55..54ce5b08b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -39,11 +39,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -54,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v1
+ uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml
new file mode 100644
index 000000000..8f0fe77d8
--- /dev/null
+++ b/.github/workflows/issue.yml
@@ -0,0 +1,30 @@
+name: Apply labels from issue
+
+on:
+ issues:
+ types: [opened, edited]
+
+jobs:
+ Process_Issue:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Parse Issue Forms Body
+ id: parse
+ uses: zentered/issue-forms-body-parser@v1.4.3
+ - name: Apply labels from tags
+ uses: actions/github-script@v6
+ env:
+ PARSED_DATA: "${{ steps.parse.outputs.data }}"
+ with:
+ script: |
+ const parsed = JSON.parse(process.env["PARSED_DATA"]);
+ const tags = parsed.tags.text;
+ console.log('Parsed tags:', tags);
+ const labels = tags.split(',').map( t => t.trim().toLowerCase() );
+ console.log('Applying labels:', labels);
+ github.rest.issues.addLabels({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels,
+ })
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 388f7f5e9..bb6f67445 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: windows-latest
name: Generate DocFX documentation
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
ref: ${{ github.events.inputs.tag }}
diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj b/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj
index 7688d0ff2..7fa26f80f 100644
--- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj
+++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/ICSharpCode.SharpZipLib.Benchmark.csproj
@@ -1,18 +1,16 @@
-
- Exe
- net6.0;netcoreapp3.1;net462
-
+
+ Exe
+ net462;net6.0
+
-
- 0.12.1
-
+
-
+
diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs
index 697e2923a..3a7beebbb 100644
--- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs
+++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Program.cs
@@ -9,9 +9,8 @@ public class MultipleRuntimes : ManualConfig
{
public MultipleRuntimes()
{
- AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net461).AsBaseline()); // NET 4.6.1
- AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp21)); // .NET Core 2.1
- AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp31)); // .NET Core 3.1
+ AddJob(Job.Default.WithToolchain(CsProjClassicNetToolchain.Net462).AsBaseline()); // NET 4.6.2
+ AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp60)); // .NET 6.0
}
}
diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs
new file mode 100644
index 000000000..0a84e0b88
--- /dev/null
+++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipFile.cs
@@ -0,0 +1,63 @@
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using ICSharpCode.SharpZipLib.Zip;
+
+namespace ICSharpCode.SharpZipLib.Benchmark.Zip
+{
+ [MemoryDiagnoser]
+ [Config(typeof(MultipleRuntimes))]
+ public class ZipFile
+ {
+ private readonly byte[] readBuffer = new byte[4096];
+ private string zipFileWithLargeAmountOfEntriesPath;
+
+ [GlobalSetup]
+ public async Task GlobalSetup()
+ {
+ SharpZipLibOptions.InflaterPoolSize = 4;
+
+ // large real-world test file from test262 repository
+ string commitSha = "2e4e0e6b8ebe3348a207144204cb6d7a5571c863";
+ zipFileWithLargeAmountOfEntriesPath = Path.Combine(Path.GetTempPath(), $"{commitSha}.zip");
+ if (!File.Exists(zipFileWithLargeAmountOfEntriesPath))
+ {
+ var uri = $"https://github.com/tc39/test262/archive/{commitSha}.zip";
+
+ Console.WriteLine("Loading test262 repository archive from {0}", uri);
+
+ using (var client = new HttpClient())
+ {
+ using (var downloadStream = await client.GetStreamAsync(uri))
+ {
+ using (var writeStream = File.OpenWrite(zipFileWithLargeAmountOfEntriesPath))
+ {
+ await downloadStream.CopyToAsync(writeStream);
+ Console.WriteLine("File downloaded and saved to {0}", zipFileWithLargeAmountOfEntriesPath);
+ }
+ }
+ }
+ }
+
+ }
+
+ [Benchmark]
+ public void ReadLargeZipFile()
+ {
+ using (var file = new SharpZipLib.Zip.ZipFile(zipFileWithLargeAmountOfEntriesPath))
+ {
+ foreach (ZipEntry entry in file)
+ {
+ using (var stream = file.GetInputStream(entry))
+ {
+ while (stream.Read(readBuffer, 0, readBuffer.Length) > 0)
+ {
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs
index eb099ebfd..2e0c057d8 100644
--- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs
+++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipInputStream.cs
@@ -1,5 +1,4 @@
-using System;
-using System.IO;
+using System.IO;
using BenchmarkDotNet.Attributes;
namespace ICSharpCode.SharpZipLib.Benchmark.Zip
@@ -15,7 +14,8 @@ public class ZipInputStream
byte[] zippedData;
byte[] readBuffer = new byte[4096];
- public ZipInputStream()
+ [GlobalSetup]
+ public void GlobalSetup()
{
using (var memoryStream = new MemoryStream())
{
diff --git a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs
index ed125c1c7..c4e8620e3 100644
--- a/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs
+++ b/benchmark/ICSharpCode.SharpZipLib.Benchmark/Zip/ZipOutputStream.cs
@@ -1,5 +1,4 @@
-using System;
-using System.IO;
+using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@@ -16,7 +15,8 @@ public class ZipOutputStream
byte[] outputBuffer;
byte[] inputBuffer;
- public ZipOutputStream()
+ [GlobalSetup]
+ public void GlobalSetup()
{
inputBuffer = new byte[ChunkSize];
outputBuffer = new byte[N];
diff --git a/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs b/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs
new file mode 100644
index 000000000..39db32e8c
--- /dev/null
+++ b/src/ICSharpCode.SharpZipLib/Core/InflaterPool.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Concurrent;
+using ICSharpCode.SharpZipLib.Zip.Compression;
+
+namespace ICSharpCode.SharpZipLib.Core
+{
+ ///
+ /// Pool for instances as they can be costly due to byte array allocations.
+ ///
+ internal sealed class InflaterPool
+ {
+ private readonly ConcurrentQueue noHeaderPool = new ConcurrentQueue();
+ private readonly ConcurrentQueue headerPool = new ConcurrentQueue();
+
+ internal static InflaterPool Instance { get; } = new InflaterPool();
+
+ private InflaterPool()
+ {
+ }
+
+ internal Inflater Rent(bool noHeader = false)
+ {
+ if (SharpZipLibOptions.InflaterPoolSize <= 0)
+ {
+ return new Inflater(noHeader);
+ }
+
+ var pool = GetPool(noHeader);
+
+ PooledInflater inf;
+ if (pool.TryDequeue(out var inflater))
+ {
+ inf = inflater;
+ inf.Reset();
+ }
+ else
+ {
+ inf = new PooledInflater(noHeader);
+ }
+
+ return inf;
+ }
+
+ internal void Return(Inflater inflater)
+ {
+ if (SharpZipLibOptions.InflaterPoolSize <= 0)
+ {
+ return;
+ }
+
+ if (!(inflater is PooledInflater pooledInflater))
+ {
+ throw new ArgumentException("Returned inflater was not a pooled one");
+ }
+
+ var pool = GetPool(inflater.noHeader);
+ if (pool.Count < SharpZipLibOptions.InflaterPoolSize)
+ {
+ pooledInflater.Reset();
+ pool.Enqueue(pooledInflater);
+ }
+ }
+
+ private ConcurrentQueue GetPool(bool noHeader) => noHeader ? noHeaderPool : headerPool;
+ }
+}
diff --git a/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs b/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs
index b8d0dd409..52f01d079 100644
--- a/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs
+++ b/src/ICSharpCode.SharpZipLib/Core/PathUtils.cs
@@ -16,6 +16,9 @@ public static class PathUtils
/// The path with the root removed if it was present; path otherwise.
public static string DropPathRoot(string path)
{
+ // No need to drop anything
+ if (path == string.Empty) return path;
+
var invalidChars = Path.GetInvalidPathChars();
// If the first character after the root is a ':', .NET < 4.6.2 throws
var cleanRootSep = path.Length >= 3 && path[1] == ':' && path[2] == ':';
@@ -26,7 +29,7 @@ public static string DropPathRoot(string path)
var cleanPath = new string(path.Take(258)
.Select( (c, i) => invalidChars.Contains(c) || (i == 2 && cleanRootSep) ? '_' : c).ToArray());
- var stripLength = Path.GetPathRoot(cleanPath).Length;
+ var stripLength = Path.GetPathRoot(cleanPath)?.Length ?? 0;
while (path.Length > stripLength && (path[stripLength] == '/' || path[stripLength] == '\\')) stripLength++;
return path.Substring(stripLength);
}
diff --git a/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs b/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs
index 20a4ded17..feca66b3d 100644
--- a/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/GZip/GzipInputStream.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Text;
+using ICSharpCode.SharpZipLib.Core;
namespace ICSharpCode.SharpZipLib.GZip
{
@@ -82,7 +83,7 @@ public GZipInputStream(Stream baseInputStream)
/// Size of the buffer to use
///
public GZipInputStream(Stream baseInputStream, int size)
- : base(baseInputStream, new Inflater(true), size)
+ : base(baseInputStream, InflaterPool.Instance.Rent(true), size)
{
}
@@ -334,7 +335,7 @@ private void ReadFooter()
int crcval = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8) | ((footer[2] & 0xff) << 16) | (footer[3] << 24);
if (crcval != (int)crc.Value)
{
- throw new GZipException("GZIP crc sum mismatch, theirs \"" + crcval + "\" and ours \"" + (int)crc.Value);
+ throw new GZipException($"GZIP crc sum mismatch, theirs \"{crcval:x8}\" and ours \"{(int)crc.Value:x8}\"");
}
// NOTE The total here is the original total modulo 2 ^ 32.
diff --git a/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs b/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs
index 264f39a87..d4f1aa4c3 100644
--- a/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs
@@ -138,6 +138,11 @@ public string FileName
}
}
+ ///
+ /// If defined, will use this time instead of the current for the output header
+ ///
+ public DateTime? ModifiedTime { get; set; }
+
#endregion Public API
#region Stream overrides
@@ -149,21 +154,47 @@ public string FileName
/// Offset of first byte in buf to write
/// Number of bytes to write
public override void Write(byte[] buffer, int offset, int count)
+ => WriteSyncOrAsync(buffer, offset, count, null).GetAwaiter().GetResult();
+
+ private async Task WriteSyncOrAsync(byte[] buffer, int offset, int count, CancellationToken? ct)
{
if (state_ == OutputState.Header)
{
- WriteHeader();
+ if (ct.HasValue)
+ {
+ await WriteHeaderAsync(ct.Value).ConfigureAwait(false);
+ }
+ else
+ {
+ WriteHeader();
+ }
}
if (state_ != OutputState.Footer)
- {
throw new InvalidOperationException("Write not permitted in current state");
- }
-
+
crc.Update(new ArraySegment(buffer, offset, count));
- base.Write(buffer, offset, count);
+
+ if (ct.HasValue)
+ {
+ await base.WriteAsync(buffer, offset, count, ct.Value).ConfigureAwait(false);
+ }
+ else
+ {
+ base.Write(buffer, offset, count);
+ }
}
+ ///
+ /// Asynchronously write given buffer to output updating crc
+ ///
+ /// Buffer to write
+ /// Offset of first byte in buf to write
+ /// Number of bytes to write
+ /// The token to monitor for cancellation requests
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct)
+ => await WriteSyncOrAsync(buffer, offset, count, ct).ConfigureAwait(false);
+
///
/// Writes remaining compressed output data to the output stream
/// and closes it.
@@ -187,7 +218,7 @@ protected override void Dispose(bool disposing)
}
}
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
///
public override async ValueTask DisposeAsync()
{
@@ -225,6 +256,16 @@ public override void Flush()
base.Flush();
}
+ ///
+ public override async Task FlushAsync(CancellationToken ct)
+ {
+ if (state_ == OutputState.Header)
+ {
+ await WriteHeaderAsync(ct).ConfigureAwait(false);
+ }
+ await base.FlushAsync(ct).ConfigureAwait(false);
+ }
+
#endregion Stream overrides
#region DeflaterOutputStream overrides
@@ -249,21 +290,13 @@ public override void Finish()
}
}
- ///
- public override async Task FlushAsync(CancellationToken ct)
- {
- await WriteHeaderAsync().ConfigureAwait(false);
- await base.FlushAsync(ct).ConfigureAwait(false);
- }
-
-
///
public override async Task FinishAsync(CancellationToken ct)
{
// If no data has been written a header should be added.
if (state_ == OutputState.Header)
{
- await WriteHeaderAsync().ConfigureAwait(false);
+ await WriteHeaderAsync(ct).ConfigureAwait(false);
}
if (state_ == OutputState.Footer)
@@ -305,7 +338,8 @@ private byte[] GetFooter()
private byte[] GetHeader()
{
- var modTime = (int)((DateTime.Now.Ticks - new DateTime(1970, 1, 1).Ticks) / 10000000L); // Ticks give back 100ns intervals
+ var modifiedUtc = ModifiedTime?.ToUniversalTime() ?? DateTime.UtcNow;
+ var modTime = (int)((modifiedUtc - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).Ticks / 10000000L); // Ticks give back 100ns intervals
byte[] gzipHeader = {
// The two magic bytes
GZipConstants.ID1,
@@ -351,12 +385,12 @@ private void WriteHeader()
baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length);
}
- private async Task WriteHeaderAsync()
+ private async Task WriteHeaderAsync(CancellationToken ct)
{
if (state_ != OutputState.Header) return;
state_ = OutputState.Footer;
var gzipHeader = GetHeader();
- await baseOutputStream_.WriteAsync(gzipHeader, 0, gzipHeader.Length).ConfigureAwait(false);
+ await baseOutputStream_.WriteAsync(gzipHeader, 0, gzipHeader.Length, ct).ConfigureAwait(false);
}
#endregion Support Routines
diff --git a/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj b/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj
index 235a41a76..49a1cd5cd 100644
--- a/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj
+++ b/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj
@@ -13,8 +13,8 @@
- 1.4.1
- $(Version).12
+ 1.4.2
+ $(Version).13
$(FileVersion)
SharpZipLib
ICSharpCode
@@ -28,7 +28,7 @@
Compression Library Zip GZip BZip2 LZW Tar
en-US
-Please see https://github.com/icsharpcode/SharpZipLib/wiki/Release-1.4.1 for more information.
+Please see https://github.com/icsharpcode/SharpZipLib/wiki/Release-1.4.2 for more information.
https://github.com/icsharpcode/SharpZipLib
diff --git a/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs b/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs
new file mode 100644
index 000000000..a6694e71e
--- /dev/null
+++ b/src/ICSharpCode.SharpZipLib/SharpZipLibOptions.cs
@@ -0,0 +1,15 @@
+using ICSharpCode.SharpZipLib.Zip.Compression;
+
+namespace ICSharpCode.SharpZipLib
+{
+ ///
+ /// Global options to alter behavior.
+ ///
+ public static class SharpZipLibOptions
+ {
+ ///
+ /// The max pool size allowed for reusing instances, defaults to 0 (disabled).
+ ///
+ public static int InflaterPoolSize { get; set; } = 0;
+ }
+}
diff --git a/src/ICSharpCode.SharpZipLib/Tar/TarBuffer.cs b/src/ICSharpCode.SharpZipLib/Tar/TarBuffer.cs
index b987f1ce0..a0f9bab80 100644
--- a/src/ICSharpCode.SharpZipLib/Tar/TarBuffer.cs
+++ b/src/ICSharpCode.SharpZipLib/Tar/TarBuffer.cs
@@ -640,7 +640,7 @@ private async ValueTask CloseAsync(CancellationToken ct, bool isAsync)
{
if (isAsync)
{
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
await outputStream.DisposeAsync().ConfigureAwait(false);
#else
outputStream.Dispose();
@@ -660,7 +660,7 @@ private async ValueTask CloseAsync(CancellationToken ct, bool isAsync)
{
if (isAsync)
{
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
await inputStream.DisposeAsync().ConfigureAwait(false);
#else
inputStream.Dispose();
diff --git a/src/ICSharpCode.SharpZipLib/Tar/TarHeader.cs b/src/ICSharpCode.SharpZipLib/Tar/TarHeader.cs
index 36d6eca44..2ef3777aa 100644
--- a/src/ICSharpCode.SharpZipLib/Tar/TarHeader.cs
+++ b/src/ICSharpCode.SharpZipLib/Tar/TarHeader.cs
@@ -869,7 +869,7 @@ public static string ParseName(ReadOnlySpan header, Encoding encoding)
}
}
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
var value = encoding.GetString(header.Slice(0, count));
#else
var value = encoding.GetString(header.ToArray(), 0, count);
diff --git a/src/ICSharpCode.SharpZipLib/Tar/TarInputStream.cs b/src/ICSharpCode.SharpZipLib/Tar/TarInputStream.cs
index c87c48d32..2cd646ae9 100644
--- a/src/ICSharpCode.SharpZipLib/Tar/TarInputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/Tar/TarInputStream.cs
@@ -232,7 +232,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel
return ReadAsync(buffer.AsMemory().Slice(offset, count), cancellationToken, true).AsTask();
}
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
///
/// Reads bytes from the current tar archive entry.
///
@@ -372,7 +372,7 @@ protected override void Dispose(bool disposing)
}
}
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
///
/// Closes this stream. Calls the TarBuffer's close() method.
/// The underlying stream is closed by the TarBuffer.
diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs
index 439b4c601..5bf2a985e 100644
--- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs
+++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs
@@ -137,7 +137,7 @@ public class Inflater
/// True means, that the inflated stream doesn't contain a Zlib header or
/// footer.
///
- private bool noHeader;
+ internal bool noHeader;
private readonly StreamManipulator input;
private OutputWindow outputWindow;
diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs
new file mode 100644
index 000000000..0828de3ef
--- /dev/null
+++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/PooledInflater.cs
@@ -0,0 +1,14 @@
+using ICSharpCode.SharpZipLib.Core;
+
+namespace ICSharpCode.SharpZipLib.Zip.Compression
+{
+ ///
+ /// A marker type for pooled version of an inflator that we can return back to .
+ ///
+ internal sealed class PooledInflater : Inflater
+ {
+ public PooledInflater(bool noHeader) : base(noHeader)
+ {
+ }
+ }
+}
diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs
index f448e0fad..a7e6807ca 100644
--- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs
@@ -240,11 +240,9 @@ protected void EncryptBlock(byte[] buffer, int offset, int length)
/// are processed.
///
protected void Deflate()
- {
- Deflate(false);
- }
+ => DeflateSyncOrAsync(false, null).GetAwaiter().GetResult();
- private void Deflate(bool flushing)
+ private async Task DeflateSyncOrAsync(bool flushing, CancellationToken? ct)
{
while (flushing || !deflater_.IsNeedingInput)
{
@@ -257,7 +255,14 @@ private void Deflate(bool flushing)
EncryptBlock(buffer_, 0, deflateCount);
- baseOutputStream_.Write(buffer_, 0, deflateCount);
+ if (ct.HasValue)
+ {
+ await baseOutputStream_.WriteAsync(buffer_, 0, deflateCount, ct.Value).ConfigureAwait(false);
+ }
+ else
+ {
+ baseOutputStream_.Write(buffer_, 0, deflateCount);
+ }
}
if (!deflater_.IsNeedingInput)
@@ -383,10 +388,23 @@ public override int Read(byte[] buffer, int offset, int count)
public override void Flush()
{
deflater_.Flush();
- Deflate(true);
+ DeflateSyncOrAsync(true, null).GetAwaiter().GetResult();
baseOutputStream_.Flush();
}
+ ///
+ /// Asynchronously clears all buffers for this stream, causes any buffered data to be written to the underlying device, and monitors cancellation requests.
+ ///
+ ///
+ /// The token to monitor for cancellation requests. The default value is .
+ ///
+ public override async Task FlushAsync(CancellationToken cancellationToken)
+ {
+ deflater_.Flush();
+ await DeflateSyncOrAsync(true, cancellationToken).ConfigureAwait(false);
+ await baseOutputStream_.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
+
///
/// Calls and closes the underlying
/// stream when is true.
@@ -417,7 +435,7 @@ protected override void Dispose(bool disposing)
}
}
-#if NETSTANDARD2_1_OR_GREATER
+#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
///
/// Calls and closes the underlying
/// stream when is true.
@@ -491,6 +509,27 @@ public override void Write(byte[] buffer, int offset, int count)
Deflate();
}
+ ///
+ /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests.
+ ///
+ ///
+ /// The byte array
+ ///
+ ///
+ /// The offset into the byte array where to start.
+ ///
+ ///
+ /// The number of bytes to write.
+ ///
+ ///
+ /// The token to monitor for cancellation requests. The default value is .
+ ///
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct)
+ {
+ deflater_.SetInput(buffer, offset, count);
+ await DeflateSyncOrAsync(false, ct).ConfigureAwait(false);
+ }
+
#endregion Stream Overrides
#region Instance Fields
diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs
index 7790474d2..980ffc701 100644
--- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Security.Cryptography;
+using ICSharpCode.SharpZipLib.Core;
namespace ICSharpCode.SharpZipLib.Zip.Compression.Streams
{
@@ -339,7 +340,7 @@ public class InflaterInputStream : Stream
/// The InputStream to read bytes from
///
public InflaterInputStream(Stream baseInputStream)
- : this(baseInputStream, new Inflater(), 4096)
+ : this(baseInputStream, InflaterPool.Instance.Rent(), 4096)
{
}
@@ -630,6 +631,12 @@ protected override void Dispose(bool disposing)
baseInputStream.Dispose();
}
}
+
+ if (inf is PooledInflater inflater)
+ {
+ InflaterPool.Instance.Return(inflater);
+ }
+ inf = null;
}
///
diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs
index 69bb9f6a9..7fc1c5592 100644
--- a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs
+++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs
@@ -314,7 +314,7 @@ public enum FileUpdateMode
/// }
///
///
- public class ZipFile : IEnumerable, IDisposable
+ public class ZipFile : IEnumerable, IDisposable
{
#region KeyHandling
@@ -387,6 +387,23 @@ private bool HaveKeys
#region Constructors
+ ///
+ /// Opens a Zip file with the given name for reading.
+ ///
+ /// The name of the file to open.
+ /// The argument supplied is null.
+ ///
+ /// An i/o error occurs
+ ///
+ ///
+ /// The file doesn't contain a valid zip archive.
+ ///
+ public ZipFile(string name) :
+ this(name, null)
+ {
+
+ }
+
///
/// Opens a Zip file with the given name for reading.
///
@@ -399,7 +416,7 @@ private bool HaveKeys
///
/// The file doesn't contain a valid zip archive.
///
- public ZipFile(string name, StringCodec stringCodec = null)
+ public ZipFile(string name, StringCodec stringCodec)
{
name_ = name ?? throw new ArgumentNullException(nameof(name));
@@ -500,6 +517,29 @@ public ZipFile(Stream stream) :
}
+ ///
+ /// Opens a Zip file reading the given .
+ ///
+ /// The to read archive data from.
+ /// true to leave the stream open when the ZipFile is disposed, false to dispose of it
+ ///
+ /// An i/o error occurs
+ ///
+ ///
+ /// The stream doesn't contain a valid zip archive.
+ ///
+ ///
+ /// The stream doesnt support seeking.
+ ///
+ ///
+ /// The stream argument is null.
+ ///
+ public ZipFile(Stream stream, bool leaveOpen) :
+ this(stream, leaveOpen, null)
+ {
+
+ }
+
///
/// Opens a Zip file reading the given .
///
@@ -518,7 +558,7 @@ public ZipFile(Stream stream) :
///
/// The stream argument is null.
///
- public ZipFile(Stream stream, bool leaveOpen, StringCodec stringCodec = null)
+ public ZipFile(Stream stream, bool leaveOpen, StringCodec stringCodec)
{
if (stream == null)
{
@@ -770,7 +810,31 @@ public StringCodec StringCodec
///
/// The Zip file has been closed.
///
- public IEnumerator GetEnumerator()
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ /// Gets an enumerator for the Zip entries in this Zip file.
+ ///
+ /// Returns an for this archive.
+ ///
+ /// The Zip file has been closed.
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ /// Gets an enumerator for the Zip entries in this Zip file.
+ ///
+ /// Returns an for this archive.
+ ///
+ /// The Zip file has been closed.
+ ///
+ public ZipEntryEnumerator GetEnumerator()
{
if (isDisposed_)
{
@@ -914,7 +978,7 @@ public Stream GetInputStream(long entryIndex)
case CompressionMethod.Deflated:
// No need to worry about ownership and closing as underlying stream close does nothing.
- result = new InflaterInputStream(result, new Inflater(true));
+ result = new InflaterInputStream(result, InflaterPool.Instance.Rent(true));
break;
case CompressionMethod.BZip2:
@@ -3961,20 +4025,26 @@ public static implicit operator string(ZipString zipString)
///
/// An enumerator for Zip entries
///
- private class ZipEntryEnumerator : IEnumerator
+ public struct ZipEntryEnumerator : IEnumerator
{
#region Constructors
+ ///
+ /// Constructs a new instance of .
+ ///
+ /// Entries to iterate.
public ZipEntryEnumerator(ZipEntry[] entries)
{
array = entries;
+ index = -1;
}
#endregion Constructors
#region IEnumerator Members
- public object Current
+ ///
+ public ZipEntry Current
{
get
{
@@ -3982,22 +4052,32 @@ public object Current
}
}
+ ///
+ object IEnumerator.Current => Current;
+
+ ///
public void Reset()
{
index = -1;
}
+ ///
public bool MoveNext()
{
return (++index < array.Length);
}
+ ///
+ public void Dispose()
+ {
+ }
+
#endregion IEnumerator Members
#region Instance Fields
private ZipEntry[] array;
- private int index = -1;
+ private int index;
#endregion Instance Fields
}
diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs
index ddfd9086b..37e9e8ba8 100644
--- a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs
@@ -1,11 +1,11 @@
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Encryption;
-using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using System;
using System.Diagnostics;
using System.IO;
-using System.Linq;
+using ICSharpCode.SharpZipLib.Core;
+using ICSharpCode.SharpZipLib.Zip.Compression;
namespace ICSharpCode.SharpZipLib.Zip
{
@@ -89,7 +89,7 @@ public class ZipInputStream : InflaterInputStream
///
/// The underlying providing data.
public ZipInputStream(Stream baseInputStream)
- : base(baseInputStream, new Inflater(true))
+ : base(baseInputStream, InflaterPool.Instance.Rent(true))
{
internalReader = new ReadDataHandler(ReadingNotAvailable);
}
@@ -100,7 +100,7 @@ public ZipInputStream(Stream baseInputStream)
/// The underlying providing data.
/// Size of the buffer.
public ZipInputStream(Stream baseInputStream, int bufferSize)
- : base(baseInputStream, new Inflater(true), bufferSize)
+ : base(baseInputStream, InflaterPool.Instance.Rent(true), bufferSize)
{
internalReader = new ReadDataHandler(ReadingNotAvailable);
}
@@ -574,9 +574,7 @@ private int InitialRead(byte[] destination, int offset, int count)
// Generate and set crypto transform...
var managed = new PkzipClassicManaged();
- Console.WriteLine($"Input Encoding: {_stringCodec.ZipCryptoEncoding.EncodingName}");
byte[] key = PkzipClassic.GenerateKeys(_stringCodec.ZipCryptoEncoding.GetBytes(password));
- Console.WriteLine($"Input Bytes: {string.Join(", ", key.Select(b => $"{b:x2}").ToArray())}");
inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null);
diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs
index 3f4d0240b..2cc36df22 100644
--- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs
+++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs
@@ -551,6 +551,8 @@ await baseOutputStream_.WriteProcToStreamAsync(s =>
///
public void CloseEntry()
{
+ // Note: This method will run synchronously
+ FinishCompressionSyncOrAsync(null).GetAwaiter().GetResult();
WriteEntryFooter(baseOutputStream_);
// Patch the header if possible
@@ -564,9 +566,41 @@ public void CloseEntry()
curEntry = null;
}
+ private async Task FinishCompressionSyncOrAsync(CancellationToken? ct)
+ {
+ // Compression handled externally
+ if (entryIsPassthrough) return;
+
+ // First finish the deflater, if appropriate
+ if (curMethod == CompressionMethod.Deflated)
+ {
+ if (size >= 0)
+ {
+ if (ct.HasValue) {
+ await base.FinishAsync(ct.Value).ConfigureAwait(false);
+ } else {
+ base.Finish();
+ }
+ }
+ else
+ {
+ deflater_.Reset();
+ }
+ }
+ if (curMethod == CompressionMethod.Stored)
+ {
+ // This is done by Finish() for Deflated entries, but we need to do it
+ // ourselves for Stored ones
+ base.GetAuthCodeIfAES();
+ }
+
+ return;
+ }
+
///
public async Task CloseEntryAsync(CancellationToken ct)
{
+ await FinishCompressionSyncOrAsync(ct).ConfigureAwait(false);
await baseOutputStream_.WriteProcToStreamAsync(WriteEntryFooter, ct).ConfigureAwait(false);
// Patch the header if possible
@@ -600,24 +634,9 @@ internal void WriteEntryFooter(Stream stream)
long csize = size;
- // First finish the deflater, if appropriate
- if (curMethod == CompressionMethod.Deflated)
+ if (curMethod == CompressionMethod.Deflated && size >= 0)
{
- if (size >= 0)
- {
- base.Finish();
- csize = deflater_.TotalOut;
- }
- else
- {
- deflater_.Reset();
- }
- }
- else if (curMethod == CompressionMethod.Stored)
- {
- // This is done by Finish() for Deflated entries, but we need to do it
- // ourselves for Stored ones
- base.GetAuthCodeIfAES();
+ csize = deflater_.TotalOut;
}
// Write the AES Authentication Code (a hash of the compressed and encrypted data)
@@ -748,9 +767,7 @@ private byte[] CreateZipCryptoHeader(long crcValue)
private void InitializeZipCryptoPassword(string password)
{
var pkManaged = new PkzipClassicManaged();
- Console.WriteLine($"Output Encoding: {ZipCryptoEncoding.EncodingName}");
byte[] key = PkzipClassic.GenerateKeys(ZipCryptoEncoding.GetBytes(password));
- Console.WriteLine($"Output Bytes: {string.Join(", ", key.Select(b => $"{b:x2}").ToArray())}");
cryptoTransform_ = pkManaged.CreateEncryptor(key, null);
}
@@ -763,6 +780,13 @@ private void InitializeZipCryptoPassword(string password)
/// Archive size is invalid
/// No entry is active.
public override void Write(byte[] buffer, int offset, int count)
+ => WriteSyncOrAsync(buffer, offset, count, null).GetAwaiter().GetResult();
+
+ ///
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct)
+ => await WriteSyncOrAsync(buffer, offset, count, ct).ConfigureAwait(false);
+
+ private async Task WriteSyncOrAsync(byte[] buffer, int offset, int count, CancellationToken? ct)
{
if (curEntry == null)
{
@@ -797,7 +821,7 @@ public override void Write(byte[] buffer, int offset, int count)
size += count;
- if(curMethod == CompressionMethod.Stored || entryIsPassthrough)
+ if (curMethod == CompressionMethod.Stored || entryIsPassthrough)
{
if (Password != null)
{
@@ -805,12 +829,26 @@ public override void Write(byte[] buffer, int offset, int count)
}
else
{
- baseOutputStream_.Write(buffer, offset, count);
+ if (ct.HasValue)
+ {
+ await baseOutputStream_.WriteAsync(buffer, offset, count, ct.Value).ConfigureAwait(false);
+ }
+ else
+ {
+ baseOutputStream_.Write(buffer, offset, count);
+ }
}
}
else
{
- base.Write(buffer, offset, count);
+ if (ct.HasValue)
+ {
+ await base.WriteAsync(buffer, offset, count, ct.Value).ConfigureAwait(false);
+ }
+ else
+ {
+ base.Write(buffer, offset, count);
+ }
}
}
diff --git a/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipAsyncTests.cs b/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipAsyncTests.cs
index 209ae15d4..b259af8cf 100644
--- a/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipAsyncTests.cs
+++ b/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipAsyncTests.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.GZip;
@@ -7,8 +8,6 @@
namespace ICSharpCode.SharpZipLib.Tests.GZip
{
-
-
[TestFixture]
public class GZipAsyncTests
{
@@ -140,5 +139,48 @@ public async Task EmptyGZipStreamAsync()
Assert.IsEmpty(content);
}
}
+
+ [Test]
+ [Category("GZip")]
+ [Category("Async")]
+ public async Task WriteGZipStreamToAsyncOnlyStream()
+ {
+#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
+ var content = Encoding.ASCII.GetBytes("a");
+ var modTime = DateTime.UtcNow;
+
+ await using (var msAsync = new MemoryStreamWithoutSync())
+ {
+ await using (var outStream = new GZipOutputStream(msAsync) { IsStreamOwner = false })
+ {
+ outStream.ModifiedTime = modTime;
+ await outStream.WriteAsync(content);
+ }
+
+ using var msSync = new MemoryStream();
+ using (var outStream = new GZipOutputStream(msSync) { IsStreamOwner = false })
+ {
+ outStream.ModifiedTime = modTime;
+ outStream.Write(content);
+ }
+
+ var syncBytes = string.Join(' ', msSync.ToArray());
+ var asyncBytes = string.Join(' ', msAsync.ToArray());
+
+ Assert.AreEqual(syncBytes, asyncBytes, "Sync and Async compressed streams are not equal");
+
+ // Since GZipInputStream isn't async yet we need to read from it from a regular MemoryStream
+ using (var readStream = new MemoryStream(msAsync.ToArray()))
+ using (var inStream = new GZipInputStream(readStream))
+ using (var reader = new StreamReader(inStream))
+ {
+ Assert.AreEqual(content, await reader.ReadToEndAsync());
+ }
+ }
+#else
+ await Task.CompletedTask;
+ Assert.Ignore("AsyncDispose is not supported");
+#endif
+ }
}
}
diff --git a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj
index 73ef2eb0d..8e9745e96 100644
--- a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj
+++ b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj
@@ -12,11 +12,11 @@
-
+
-
-
-
+
+
+
diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Streams.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Streams.cs
index f6b0fff3e..2d1b00fb8 100644
--- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Streams.cs
+++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Streams.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Threading;
+using System.Threading.Tasks;
namespace ICSharpCode.SharpZipLib.Tests.TestSupport
{
@@ -188,6 +189,77 @@ public override long Position
}
+#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
+ ///
+ /// A that does not support non-async operations.
+ ///
+ ///
+ /// This could not be done by extending MemoryStream itself, since other instances of MemoryStream tries to us a faster (non-async) method of copying
+ /// if it detects that it's a (subclass of) MemoryStream.
+ ///
+ public class MemoryStreamWithoutSync : Stream
+ {
+ MemoryStream _inner = new MemoryStream();
+
+ public override bool CanRead => _inner.CanRead;
+ public override bool CanSeek => _inner.CanSeek;
+ public override bool CanWrite => _inner.CanWrite;
+ public override long Length => _inner.Length;
+ public override long Position { get => _inner.Position; set => _inner.Position = value; }
+
+ public byte[] ToArray() => _inner.ToArray();
+
+ public override void Flush() => throw new NotSupportedException($"Non-async call to {nameof(Flush)}");
+
+
+ public override void CopyTo(Stream destination, int bufferSize) => throw new NotSupportedException($"Non-async call to {nameof(CopyTo)}");
+ public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException($"Non-async call to {nameof(Write)}");
+ public override int Read(Span buffer) => throw new NotSupportedException($"Non-async call to {nameof(Read)}");
+
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException($"Non-async call to {nameof(Write)}");
+ public override void WriteByte(byte value) => throw new NotSupportedException($"Non-async call to {nameof(Write)}");
+
+ public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException($"Non-async call to {nameof(Read)}");
+ public override int ReadByte() => throw new NotSupportedException($"Non-async call to {nameof(ReadByte)}");
+
+ // Even though our mock stream is writing synchronously, this should not fail the tests
+ public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ var buf = new byte[bufferSize];
+ while(_inner.Read(buf, 0, bufferSize) > 0) {
+ await destination.WriteAsync(buf, cancellationToken);
+ }
+ }
+ public override Task FlushAsync(CancellationToken cancellationToken) => TaskFromBlocking(() => _inner.Flush());
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => TaskFromBlocking(() => _inner.Write(buffer, offset, count));
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => Task.FromResult(_inner.Read(buffer, offset, count));
+ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => ValueTaskFromBlocking(() => _inner.Write(buffer.Span));
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => ValueTask.FromResult(_inner.Read(buffer.Span));
+
+ static Task TaskFromBlocking(Action action)
+ {
+ action();
+ return Task.CompletedTask;
+ }
+
+ static ValueTask ValueTaskFromBlocking(Action action)
+ {
+ action();
+ return ValueTask.CompletedTask;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ return _inner.Seek(offset, origin);
+ }
+
+ public override void SetLength(long value)
+ {
+ _inner.SetLength(value);
+ }
+ }
+#endif
+
///
/// A that cannot be read but supports infinite writes.
///
diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs
index e594cd17f..c25059da4 100644
--- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs
+++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs
@@ -1815,5 +1815,27 @@ public void TestDescriptorUpdateOnAdd(UseZip64 useZip64)
}
}
}
+
+ ///
+ /// Check that Zip files can be created with an empty file name
+ ///
+ [Test]
+ [Category("Zip")]
+ public void HandlesEmptyFileName()
+ {
+ using var ms = new MemoryStream();
+ using (var zos = new ZipOutputStream(ms){IsStreamOwner = false})
+ {
+ zos.PutNextEntry(new ZipEntry(String.Empty));
+ Utils.WriteDummyData(zos, 64);
+ }
+ ms.Seek(0, SeekOrigin.Begin);
+ using (var zis = new ZipInputStream(ms){IsStreamOwner = false})
+ {
+ var entry = zis.GetNextEntry();
+ Assert.That(entry.Name, Is.Empty);
+ Assert.That(zis.ReadBytes(64).Length, Is.EqualTo(64));
+ }
+ }
}
}
diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs
index aff027bf1..d228e5ee4 100644
--- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs
+++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs
@@ -15,7 +15,7 @@ public class ZipStreamAsyncTests
[Category("Async")]
public async Task WriteZipStreamUsingAsync()
{
-#if NETCOREAPP3_1_OR_GREATER
+#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
await using var ms = new MemoryStream();
await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false})
@@ -121,5 +121,34 @@ public async Task WriteReadOnlyZipStreamAsync ()
ZipTesting.AssertValidZip(new MemoryStream(ms.ToArray()));
}
+ [Test]
+ [Category("Zip")]
+ [Category("Async")]
+ [TestCase(12, Description = "Small files")]
+ [TestCase(12000, Description = "Large files")]
+ public async Task WriteZipStreamToAsyncOnlyStream (int fileSize)
+ {
+#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
+ await using(var ms = new MemoryStreamWithoutSync()){
+ await using(var outStream = new ZipOutputStream(ms) { IsStreamOwner = false })
+ {
+ await outStream.PutNextEntryAsync(new ZipEntry("FirstFile"));
+ await Utils.WriteDummyDataAsync(outStream, fileSize);
+
+ await outStream.PutNextEntryAsync(new ZipEntry("SecondFile"));
+ await Utils.WriteDummyDataAsync(outStream, fileSize);
+
+ await outStream.FinishAsync(CancellationToken.None);
+ await outStream.DisposeAsync();
+ }
+
+ ZipTesting.AssertValidZip(new MemoryStream(ms.ToArray()));
+ }
+#else
+ await Task.CompletedTask;
+ Assert.Ignore("AsyncDispose is not supported");
+#endif
+ }
+
}
}
\ No newline at end of file