diff --git a/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs b/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs index 0b1a647fe..b15e7a5b4 100644 --- a/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs @@ -3,7 +3,9 @@ using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using System; using System.IO; -using System.Text; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.GZip { @@ -162,6 +164,26 @@ public override void Write(byte[] buffer, int offset, int count) base.Write(buffer, offset, count); } +#if NETSTANDARD2_1_OR_GREATER + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + + if (state_ == OutputState.Header) + { + await WriteHeaderAsync(); + } + + if (state_ != OutputState.Footer) + { + throw new InvalidOperationException("Write not permitted in current state"); + } + + crc.Update(new ArraySegment(buffer, offset, count)); + await base.WriteAsync(buffer, offset, count, cancellationToken); + } +#endif + /// /// Writes remaining compressed output data to the output stream /// and closes it. @@ -184,6 +206,30 @@ protected override void Dispose(bool disposing) } } } + +#if NETSTANDARD2_1_OR_GREATER + /// + public override async ValueTask DisposeAsync() + { + try + { + await FinishAsync(CancellationToken.None); + } + finally + { + if (state_ != OutputState.Closed) + { + state_ = OutputState.Closed; + if (IsStreamOwner) + { + await baseOutputStream_.DisposeAsync(); + } + } + + await base.DisposeAsync(); + } + } +#endif /// /// Flushes the stream by ensuring the header is written, and then calling Flush @@ -218,75 +264,117 @@ public override void Finish() { state_ = OutputState.Finished; base.Finish(); + + byte[] gzipFooter = GetFooter(); - var totalin = (uint)(deflater_.TotalIn & 0xffffffff); - var crcval = (uint)(crc.Value & 0xffffffff); - - byte[] gzipFooter; - - unchecked - { - gzipFooter = new byte[] { - (byte) crcval, (byte) (crcval >> 8), - (byte) (crcval >> 16), (byte) (crcval >> 24), + baseOutputStream_.Write(gzipFooter, 0, gzipFooter.Length); + } + } - (byte) totalin, (byte) (totalin >> 8), - (byte) (totalin >> 16), (byte) (totalin >> 24) - }; - } +#if NETSTANDARD2_1_OR_GREATER + /// + public override async Task FinishAsync(CancellationToken ct) + { + // If no data has been written a header should be added. + if (state_ == OutputState.Header) + { + await WriteHeaderAsync(); + } - baseOutputStream_.Write(gzipFooter, 0, gzipFooter.Length); + if (state_ == OutputState.Footer) + { + state_ = OutputState.Finished; + await base.FinishAsync(ct); + await baseOutputStream_.WriteAsync(GetFooter(), ct); } } +#endif #endregion DeflaterOutputStream overrides #region Support Routines - private static string CleanFilename(string path) - => path.Substring(path.LastIndexOf('/') + 1); - - private void WriteHeader() + private byte[] GetFooter() { - if (state_ == OutputState.Header) - { - state_ = OutputState.Footer; + var totalin = (uint)(deflater_.TotalIn & 0xffffffff); + var crcval = (uint)(crc.Value & 0xffffffff); - var mod_time = (int)((DateTime.Now.Ticks - new DateTime(1970, 1, 1).Ticks) / 10000000L); // Ticks give back 100ns intervals - byte[] gzipHeader = { - // The two magic bytes - GZipConstants.ID1, - GZipConstants.ID2, + byte[] gzipFooter; + + unchecked + { + gzipFooter = new [] { + (byte) crcval, + (byte) (crcval >> 8), + (byte) (crcval >> 16), + (byte) (crcval >> 24), + (byte) totalin, + (byte) (totalin >> 8), + (byte) (totalin >> 16), + (byte) (totalin >> 24), + }; + } - // The compression type - GZipConstants.CompressionMethodDeflate, + return gzipFooter; + } - // The flags (not set) - (byte)flags, + private byte[] GetHeader() + { + var modTime = (int)((DateTime.Now.Ticks - new DateTime(1970, 1, 1).Ticks) / 10000000L); // Ticks give back 100ns intervals + byte[] gzipHeader = { + // The two magic bytes + GZipConstants.ID1, + GZipConstants.ID2, - // The modification time - (byte) mod_time, (byte) (mod_time >> 8), - (byte) (mod_time >> 16), (byte) (mod_time >> 24), + // The compression type + GZipConstants.CompressionMethodDeflate, - // The extra flags - 0, + // The flags (not set) + (byte)flags, - // The OS type (unknown) - 255 - }; + // The modification time + (byte) modTime, (byte) (modTime >> 8), + (byte) (modTime >> 16), (byte) (modTime >> 24), - baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length); + // The extra flags + 0, - if (flags.HasFlag(GZipFlags.FNAME)) - { - var fname = GZipConstants.Encoding.GetBytes(fileName); - baseOutputStream_.Write(fname, 0, fname.Length); + // The OS type (unknown) + 255 + }; - // End filename string with a \0 - baseOutputStream_.Write(new byte[] { 0 }, 0, 1); - } + if (!flags.HasFlag(GZipFlags.FNAME)) + { + return gzipHeader; } + + + return gzipHeader + .Concat(GZipConstants.Encoding.GetBytes(fileName)) + .Concat(new byte []{0}) // End filename string with a \0 + .ToArray(); + } + + private static string CleanFilename(string path) + => path.Substring(path.LastIndexOf('/') + 1); + + private void WriteHeader() + { + if (state_ != OutputState.Header) return; + state_ = OutputState.Footer; + + var gzipHeader = GetHeader(); + baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length); + } + + #if NETSTANDARD2_1_OR_GREATER + private async ValueTask WriteHeaderAsync() + { + if (state_ != OutputState.Header) return; + state_ = OutputState.Footer; + await baseOutputStream_.WriteAsync(GetHeader()); } + #endif #endregion Support Routines } diff --git a/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipTests.cs b/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipTests.cs index 62be609fc..b7ee7e444 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipTests.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/GZip/GZipTests.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Tests.GZip { @@ -428,7 +430,50 @@ public void SmallBufferDecompression() } } + +#if NETCOREAPP3_1_OR_GREATER + [Test] + [Category("GZip")] + [Category("Async")] + public async Task SmallBufferDecompressionAsync() + { + var outputBufferSize = 100000; + var inputBufferSize = outputBufferSize * 4; + var inputBuffer = Utils.GetDummyBytes(inputBufferSize, seed: 0); + + var outputBuffer = new byte[outputBufferSize]; + await using var msGzip = new MemoryStream(); + await using (var gzos = new GZipOutputStream(msGzip){IsStreamOwner = false}) + { + await gzos.WriteAsync(inputBuffer, 0, inputBuffer.Length); + } + + msGzip.Seek(0, SeekOrigin.Begin); + + + using (var gzis = new GZipInputStream(msGzip)) + await using (var msRaw = new MemoryStream()) + { + + int readOut; + while ((readOut = await gzis.ReadAsync(outputBuffer, 0, outputBuffer.Length)) > 0) + { + await msRaw.WriteAsync(outputBuffer, 0, readOut); + } + + var resultBuffer = msRaw.ToArray(); + + for (var i = 0; i < resultBuffer.Length; i++) + { + Assert.AreEqual(inputBuffer[i], resultBuffer[i]); + } + + + } + } +#endif + /// /// Should gracefully handle reading from a stream that becomes unreadable after /// all of the data has been read.