Skip to content

Commit e63c41f

Browse files
committed
feat(gzip): add GzipOutputStream async support
1 parent e1e1a91 commit e63c41f

File tree

2 files changed

+181
-48
lines changed

2 files changed

+181
-48
lines changed

src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs

Lines changed: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
44
using System;
55
using System.IO;
6-
using System.Text;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
79

810
namespace ICSharpCode.SharpZipLib.GZip
911
{
@@ -162,6 +164,26 @@ public override void Write(byte[] buffer, int offset, int count)
162164
base.Write(buffer, offset, count);
163165
}
164166

167+
#if NETSTANDARD2_1_OR_GREATER
168+
/// <inheritdoc cref="DeflaterOutputStream.WriteAsync(byte[],int,int,CancellationToken)"/>
169+
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
170+
{
171+
172+
if (state_ == OutputState.Header)
173+
{
174+
await WriteHeaderAsync();
175+
}
176+
177+
if (state_ != OutputState.Footer)
178+
{
179+
throw new InvalidOperationException("Write not permitted in current state");
180+
}
181+
182+
crc.Update(new ArraySegment<byte>(buffer, offset, count));
183+
await base.WriteAsync(buffer, offset, count, cancellationToken);
184+
}
185+
#endif
186+
165187
/// <summary>
166188
/// Writes remaining compressed output data to the output stream
167189
/// and closes it.
@@ -184,6 +206,30 @@ protected override void Dispose(bool disposing)
184206
}
185207
}
186208
}
209+
210+
#if NETSTANDARD2_1_OR_GREATER
211+
/// <inheritdoc cref="DeflaterOutputStream.DisposeAsync"/>
212+
public override async ValueTask DisposeAsync()
213+
{
214+
try
215+
{
216+
await FinishAsync(CancellationToken.None);
217+
}
218+
finally
219+
{
220+
if (state_ != OutputState.Closed)
221+
{
222+
state_ = OutputState.Closed;
223+
if (IsStreamOwner)
224+
{
225+
await baseOutputStream_.DisposeAsync();
226+
}
227+
}
228+
229+
await base.DisposeAsync();
230+
}
231+
}
232+
#endif
187233

188234
/// <summary>
189235
/// Flushes the stream by ensuring the header is written, and then calling <see cref="DeflaterOutputStream.Flush">Flush</see>
@@ -218,75 +264,117 @@ public override void Finish()
218264
{
219265
state_ = OutputState.Finished;
220266
base.Finish();
267+
268+
byte[] gzipFooter = GetFooter();
221269

222-
var totalin = (uint)(deflater_.TotalIn & 0xffffffff);
223-
var crcval = (uint)(crc.Value & 0xffffffff);
224-
225-
byte[] gzipFooter;
226-
227-
unchecked
228-
{
229-
gzipFooter = new byte[] {
230-
(byte) crcval, (byte) (crcval >> 8),
231-
(byte) (crcval >> 16), (byte) (crcval >> 24),
270+
baseOutputStream_.Write(gzipFooter, 0, gzipFooter.Length);
271+
}
272+
}
232273

233-
(byte) totalin, (byte) (totalin >> 8),
234-
(byte) (totalin >> 16), (byte) (totalin >> 24)
235-
};
236-
}
274+
#if NETSTANDARD2_1_OR_GREATER
275+
/// <inheritdoc cref="Finish"/>
276+
public override async Task FinishAsync(CancellationToken ct)
277+
{
278+
// If no data has been written a header should be added.
279+
if (state_ == OutputState.Header)
280+
{
281+
await WriteHeaderAsync();
282+
}
237283

238-
baseOutputStream_.Write(gzipFooter, 0, gzipFooter.Length);
284+
if (state_ == OutputState.Footer)
285+
{
286+
state_ = OutputState.Finished;
287+
await base.FinishAsync(ct);
288+
await baseOutputStream_.WriteAsync(GetFooter(), ct);
239289
}
240290
}
291+
#endif
241292

242293
#endregion DeflaterOutputStream overrides
243294

244295
#region Support Routines
245296

246-
private static string CleanFilename(string path)
247-
=> path.Substring(path.LastIndexOf('/') + 1);
248-
249-
private void WriteHeader()
297+
private byte[] GetFooter()
250298
{
251-
if (state_ == OutputState.Header)
252-
{
253-
state_ = OutputState.Footer;
299+
var totalin = (uint)(deflater_.TotalIn & 0xffffffff);
300+
var crcval = (uint)(crc.Value & 0xffffffff);
254301

255-
var mod_time = (int)((DateTime.Now.Ticks - new DateTime(1970, 1, 1).Ticks) / 10000000L); // Ticks give back 100ns intervals
256-
byte[] gzipHeader = {
257-
// The two magic bytes
258-
GZipConstants.ID1,
259-
GZipConstants.ID2,
302+
byte[] gzipFooter;
303+
304+
unchecked
305+
{
306+
gzipFooter = new [] {
307+
(byte) crcval,
308+
(byte) (crcval >> 8),
309+
(byte) (crcval >> 16),
310+
(byte) (crcval >> 24),
311+
(byte) totalin,
312+
(byte) (totalin >> 8),
313+
(byte) (totalin >> 16),
314+
(byte) (totalin >> 24),
315+
};
316+
}
260317

261-
// The compression type
262-
GZipConstants.CompressionMethodDeflate,
318+
return gzipFooter;
319+
}
263320

264-
// The flags (not set)
265-
(byte)flags,
321+
private byte[] GetHeader()
322+
{
323+
var modTime = (int)((DateTime.Now.Ticks - new DateTime(1970, 1, 1).Ticks) / 10000000L); // Ticks give back 100ns intervals
324+
byte[] gzipHeader = {
325+
// The two magic bytes
326+
GZipConstants.ID1,
327+
GZipConstants.ID2,
266328

267-
// The modification time
268-
(byte) mod_time, (byte) (mod_time >> 8),
269-
(byte) (mod_time >> 16), (byte) (mod_time >> 24),
329+
// The compression type
330+
GZipConstants.CompressionMethodDeflate,
270331

271-
// The extra flags
272-
0,
332+
// The flags (not set)
333+
(byte)flags,
273334

274-
// The OS type (unknown)
275-
255
276-
};
335+
// The modification time
336+
(byte) modTime, (byte) (modTime >> 8),
337+
(byte) (modTime >> 16), (byte) (modTime >> 24),
277338

278-
baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length);
339+
// The extra flags
340+
0,
279341

280-
if (flags.HasFlag(GZipFlags.FNAME))
281-
{
282-
var fname = GZipConstants.Encoding.GetBytes(fileName);
283-
baseOutputStream_.Write(fname, 0, fname.Length);
342+
// The OS type (unknown)
343+
255
344+
};
284345

285-
// End filename string with a \0
286-
baseOutputStream_.Write(new byte[] { 0 }, 0, 1);
287-
}
346+
if (!flags.HasFlag(GZipFlags.FNAME))
347+
{
348+
return gzipHeader;
288349
}
350+
351+
352+
return gzipHeader
353+
.Concat(GZipConstants.Encoding.GetBytes(fileName))
354+
.Concat(new byte []{0}) // End filename string with a \0
355+
.ToArray();
356+
}
357+
358+
private static string CleanFilename(string path)
359+
=> path.Substring(path.LastIndexOf('/') + 1);
360+
361+
private void WriteHeader()
362+
{
363+
if (state_ != OutputState.Header) return;
364+
state_ = OutputState.Footer;
365+
366+
var gzipHeader = GetHeader();
367+
baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length);
368+
}
369+
370+
#if NETSTANDARD2_1_OR_GREATER
371+
private async ValueTask WriteHeaderAsync()
372+
{
373+
if (state_ != OutputState.Header) return;
374+
state_ = OutputState.Footer;
375+
await baseOutputStream_.WriteAsync(GetHeader());
289376
}
377+
#endif
290378

291379
#endregion Support Routines
292380
}

test/ICSharpCode.SharpZipLib.Tests/GZip/GZipTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.IO;
66
using System.Text;
7+
using System.Threading;
8+
using System.Threading.Tasks;
79

810
namespace ICSharpCode.SharpZipLib.Tests.GZip
911
{
@@ -428,7 +430,50 @@ public void SmallBufferDecompression()
428430

429431
}
430432
}
433+
434+
#if NETCOREAPP3_1_OR_GREATER
435+
[Test]
436+
[Category("GZip")]
437+
[Category("Async")]
438+
public async Task SmallBufferDecompressionAsync()
439+
{
440+
var outputBufferSize = 100000;
441+
var inputBufferSize = outputBufferSize * 4;
442+
var inputBuffer = Utils.GetDummyBytes(inputBufferSize, seed: 0);
443+
444+
var outputBuffer = new byte[outputBufferSize];
431445

446+
await using var msGzip = new MemoryStream();
447+
await using (var gzos = new GZipOutputStream(msGzip){IsStreamOwner = false})
448+
{
449+
await gzos.WriteAsync(inputBuffer, 0, inputBuffer.Length);
450+
}
451+
452+
msGzip.Seek(0, SeekOrigin.Begin);
453+
454+
455+
using (var gzis = new GZipInputStream(msGzip))
456+
await using (var msRaw = new MemoryStream())
457+
{
458+
459+
int readOut;
460+
while ((readOut = await gzis.ReadAsync(outputBuffer, 0, outputBuffer.Length)) > 0)
461+
{
462+
await msRaw.WriteAsync(outputBuffer, 0, readOut);
463+
}
464+
465+
var resultBuffer = msRaw.ToArray();
466+
467+
for (var i = 0; i < resultBuffer.Length; i++)
468+
{
469+
Assert.AreEqual(inputBuffer[i], resultBuffer[i]);
470+
}
471+
472+
473+
}
474+
}
475+
#endif
476+
432477
/// <summary>
433478
/// Should gracefully handle reading from a stream that becomes unreadable after
434479
/// all of the data has been read.

0 commit comments

Comments
 (0)