Skip to content

Commit cbb9e83

Browse files
authored
zip: finish stream async when async (#808)
1 parent 77c5a97 commit cbb9e83

File tree

4 files changed

+137
-19
lines changed

4 files changed

+137
-19
lines changed

src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ protected override void Dispose(bool disposing)
417417
}
418418
}
419419

420-
#if NETSTANDARD2_1_OR_GREATER
420+
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
421421
/// <summary>
422422
/// Calls <see cref="FinishAsync"/> and closes the underlying
423423
/// stream when <see cref="IsStreamOwner"></see> is true.

src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,8 @@ await baseOutputStream_.WriteProcToStreamAsync(s =>
551551
/// </exception>
552552
public void CloseEntry()
553553
{
554+
// Note: This method will run synchronously
555+
FinishCompression(null).Wait();
554556
WriteEntryFooter(baseOutputStream_);
555557

556558
// Patch the header if possible
@@ -564,9 +566,41 @@ public void CloseEntry()
564566
curEntry = null;
565567
}
566568

569+
private async Task FinishCompression(CancellationToken? ct)
570+
{
571+
// Compression handled externally
572+
if (entryIsPassthrough) return;
573+
574+
// First finish the deflater, if appropriate
575+
if (curMethod == CompressionMethod.Deflated)
576+
{
577+
if (size >= 0)
578+
{
579+
if (ct.HasValue) {
580+
await base.FinishAsync(ct.Value).ConfigureAwait(false);
581+
} else {
582+
base.Finish();
583+
}
584+
}
585+
else
586+
{
587+
deflater_.Reset();
588+
}
589+
}
590+
if (curMethod == CompressionMethod.Stored)
591+
{
592+
// This is done by Finish() for Deflated entries, but we need to do it
593+
// ourselves for Stored ones
594+
base.GetAuthCodeIfAES();
595+
}
596+
597+
return;
598+
}
599+
567600
/// <inheritdoc cref="CloseEntry"/>
568601
public async Task CloseEntryAsync(CancellationToken ct)
569602
{
603+
await FinishCompression(ct).ConfigureAwait(false);
570604
await baseOutputStream_.WriteProcToStreamAsync(WriteEntryFooter, ct).ConfigureAwait(false);
571605

572606
// Patch the header if possible
@@ -600,24 +634,9 @@ internal void WriteEntryFooter(Stream stream)
600634

601635
long csize = size;
602636

603-
// First finish the deflater, if appropriate
604-
if (curMethod == CompressionMethod.Deflated)
605-
{
606-
if (size >= 0)
607-
{
608-
base.Finish();
609-
csize = deflater_.TotalOut;
610-
}
611-
else
612-
{
613-
deflater_.Reset();
614-
}
615-
}
616-
else if (curMethod == CompressionMethod.Stored)
637+
if (curMethod == CompressionMethod.Deflated && size >= 0)
617638
{
618-
// This is done by Finish() for Deflated entries, but we need to do it
619-
// ourselves for Stored ones
620-
base.GetAuthCodeIfAES();
639+
csize = deflater_.TotalOut;
621640
}
622641

623642
// Write the AES Authentication Code (a hash of the compressed and encrypted data)

test/ICSharpCode.SharpZipLib.Tests/TestSupport/Streams.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.IO;
33
using System.Threading;
4+
using System.Threading.Tasks;
45

56
namespace ICSharpCode.SharpZipLib.Tests.TestSupport
67
{
@@ -188,6 +189,77 @@ public override long Position
188189

189190
}
190191

192+
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
193+
/// <summary>
194+
/// A <see cref="Stream"/> that does not support non-async operations.
195+
/// </summary>
196+
/// <remarks>
197+
/// This could not be done by extending MemoryStream itself, since other instances of MemoryStream tries to us a faster (non-async) method of copying
198+
/// if it detects that it's a (subclass of) MemoryStream.
199+
/// </remarks>
200+
public class MemoryStreamWithoutSync : Stream
201+
{
202+
MemoryStream _inner = new MemoryStream();
203+
204+
public override bool CanRead => _inner.CanRead;
205+
public override bool CanSeek => _inner.CanSeek;
206+
public override bool CanWrite => _inner.CanWrite;
207+
public override long Length => _inner.Length;
208+
public override long Position { get => _inner.Position; set => _inner.Position = value; }
209+
210+
public byte[] ToArray() => _inner.ToArray();
211+
212+
public override void Flush() => throw new NotSupportedException($"Non-async call to {nameof(Flush)}");
213+
214+
215+
public override void CopyTo(Stream destination, int bufferSize) => throw new NotSupportedException($"Non-async call to {nameof(CopyTo)}");
216+
public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException($"Non-async call to {nameof(Write)}");
217+
public override int Read(Span<byte> buffer) => throw new NotSupportedException($"Non-async call to {nameof(Read)}");
218+
219+
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException($"Non-async call to {nameof(Write)}");
220+
public override void WriteByte(byte value) => throw new NotSupportedException($"Non-async call to {nameof(Write)}");
221+
222+
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException($"Non-async call to {nameof(Read)}");
223+
public override int ReadByte() => throw new NotSupportedException($"Non-async call to {nameof(ReadByte)}");
224+
225+
// Even though our mock stream is writing synchronously, this should not fail the tests
226+
public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
227+
{
228+
var buf = new byte[bufferSize];
229+
while(_inner.Read(buf, 0, bufferSize) > 0) {
230+
await destination.WriteAsync(buf, cancellationToken);
231+
}
232+
}
233+
public override Task FlushAsync(CancellationToken cancellationToken) => TaskFromBlocking(() => _inner.Flush());
234+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => TaskFromBlocking(() => _inner.Write(buffer, offset, count));
235+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => Task.FromResult(_inner.Read(buffer, offset, count));
236+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => ValueTaskFromBlocking(() => _inner.Write(buffer.Span));
237+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => ValueTask.FromResult(_inner.Read(buffer.Span));
238+
239+
static Task TaskFromBlocking(Action action)
240+
{
241+
action();
242+
return Task.CompletedTask;
243+
}
244+
245+
static ValueTask ValueTaskFromBlocking(Action action)
246+
{
247+
action();
248+
return ValueTask.CompletedTask;
249+
}
250+
251+
public override long Seek(long offset, SeekOrigin origin)
252+
{
253+
return _inner.Seek(offset, origin);
254+
}
255+
256+
public override void SetLength(long value)
257+
{
258+
_inner.SetLength(value);
259+
}
260+
}
261+
#endif
262+
191263
/// <summary>
192264
/// A <see cref="Stream"/> that cannot be read but supports infinite writes.
193265
/// </summary>

test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class ZipStreamAsyncTests
1515
[Category("Async")]
1616
public async Task WriteZipStreamUsingAsync()
1717
{
18-
#if NETCOREAPP3_1_OR_GREATER
18+
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
1919
await using var ms = new MemoryStream();
2020

2121
await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false})
@@ -121,5 +121,32 @@ public async Task WriteReadOnlyZipStreamAsync ()
121121
ZipTesting.AssertValidZip(new MemoryStream(ms.ToArray()));
122122
}
123123

124+
[Test]
125+
[Category("Zip")]
126+
[Category("Async")]
127+
public async Task WriteZipStreamToAsyncOnlyStream ()
128+
{
129+
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
130+
await using(var ms = new MemoryStreamWithoutSync()){
131+
await using(var outStream = new ZipOutputStream(ms) { IsStreamOwner = false })
132+
{
133+
await outStream.PutNextEntryAsync(new ZipEntry("FirstFile"));
134+
await Utils.WriteDummyDataAsync(outStream, 12);
135+
136+
await outStream.PutNextEntryAsync(new ZipEntry("SecondFile"));
137+
await Utils.WriteDummyDataAsync(outStream, 12);
138+
139+
await outStream.FinishAsync(CancellationToken.None);
140+
await outStream.DisposeAsync();
141+
}
142+
143+
ZipTesting.AssertValidZip(new MemoryStream(ms.ToArray()));
144+
}
145+
#else
146+
await Task.CompletedTask;
147+
Assert.Ignore("AsyncDispose is not supported");
148+
#endif
149+
}
150+
124151
}
125152
}

0 commit comments

Comments
 (0)