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