Skip to content

Commit

Permalink
feat(gzip): add GzipOutputStream async support (icsharpcode#672)
Browse files Browse the repository at this point in the history
  • Loading branch information
piksel authored Sep 15, 2022
1 parent 519ed73 commit b314d3d
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 71 deletions.
171 changes: 121 additions & 50 deletions src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -184,6 +186,30 @@ protected override void Dispose(bool disposing)
}
}
}

#if NETSTANDARD2_1_OR_GREATER
/// <inheritdoc cref="DeflaterOutputStream.DisposeAsync"/>
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

/// <summary>
/// Flushes the stream by ensuring the header is written, and then calling <see cref="DeflaterOutputStream.Flush">Flush</see>
Expand Down Expand Up @@ -218,74 +244,119 @@ public override void Finish()
{
state_ = OutputState.Finished;
base.Finish();

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),

(byte) totalin, (byte) (totalin >> 8),
(byte) (totalin >> 16), (byte) (totalin >> 24)
};
}

var gzipFooter = GetFooter();
baseOutputStream_.Write(gzipFooter, 0, gzipFooter.Length);
}
}

/// <inheritdoc cref="Flush"/>
public override async Task FlushAsync(CancellationToken ct)
{
await WriteHeaderAsync();
await base.FlushAsync(ct);
}


/// <inheritdoc cref="Finish"/>
public override async Task FinishAsync(CancellationToken ct)
{
// If no data has been written a header should be added.
if (state_ == OutputState.Header)
{
await WriteHeaderAsync();
}

if (state_ == OutputState.Footer)
{
state_ = OutputState.Finished;
await base.FinishAsync(ct);
var gzipFooter = GetFooter();
await baseOutputStream_.WriteAsync(gzipFooter, 0, gzipFooter.Length, ct);
}
}

#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;

// The compression type
GZipConstants.CompressionMethodDeflate,
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 flags (not set)
(byte)flags,
return gzipFooter;
}

// The modification time
(byte) mod_time, (byte) (mod_time >> 8),
(byte) (mod_time >> 16), (byte) (mod_time >> 24),
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 extra flags
0,
// The compression type
GZipConstants.CompressionMethodDeflate,

// The OS type (unknown)
255
};
// The flags (not set)
(byte)flags,

baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length);
// The modification time
(byte) modTime, (byte) (modTime >> 8),
(byte) (modTime >> 16), (byte) (modTime >> 24),

if (flags.HasFlag(GZipFlags.FNAME))
{
var fname = GZipConstants.Encoding.GetBytes(fileName);
baseOutputStream_.Write(fname, 0, fname.Length);
// The extra flags
0,

// End filename string with a \0
baseOutputStream_.Write(new byte[] { 0 }, 0, 1);
}
// The OS type (unknown)
255
};

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);
}

private async Task WriteHeaderAsync()
{
if (state_ != OutputState.Header) return;
state_ = OutputState.Footer;
var gzipHeader = GetHeader();
await baseOutputStream_.WriteAsync(gzipHeader, 0, gzipHeader.Length);
}

#endregion Support Routines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ protected override void Dispose(bool disposing)
}
}

#if NETSTANDARD2_1
#if NETSTANDARD2_1_OR_GREATER
/// <summary>
/// Calls <see cref="FinishAsync"/> and closes the underlying
/// stream when <see cref="IsStreamOwner"></see> is true.
Expand Down
144 changes: 144 additions & 0 deletions test/ICSharpCode.SharpZipLib.Tests/GZip/GZipAsyncTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tests.TestSupport;
using NUnit.Framework;

namespace ICSharpCode.SharpZipLib.Tests.GZip
{


[TestFixture]
public class GZipAsyncTests
{
[Test]
[Category("GZip")]
[Category("Async")]
public async Task SmallBufferDecompressionAsync([Values(0, 1, 3)] int seed)
{
var outputBufferSize = 100000;
var outputBuffer = new byte[outputBufferSize];
var inputBuffer = Utils.GetDummyBytes(outputBufferSize * 4, seed);

#if NETCOREAPP3_1_OR_GREATER
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 = gzis.Read(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]);
}
}
#else
using var msGzip = new MemoryStream();
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))
using (var msRaw = new MemoryStream())
{
int readOut;
while ((readOut = gzis.Read(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
}

/// <summary>
/// Basic compress/decompress test
/// </summary>
[Test]
[Category("GZip")]
[Category("Async")]
public async Task OriginalFilenameAsync()
{
var content = "FileContents";

#if NETCOREAPP3_1_OR_GREATER
await using var ms = new MemoryStream();
await using (var outStream = new GZipOutputStream(ms) { IsStreamOwner = false })
{
outStream.FileName = "/path/to/file.ext";
outStream.Write(Encoding.ASCII.GetBytes(content));
}
#else
var ms = new MemoryStream();
var outStream = new GZipOutputStream(ms){ IsStreamOwner = false };
outStream.FileName = "/path/to/file.ext";
var bytes = Encoding.ASCII.GetBytes(content);
outStream.Write(bytes, 0, bytes.Length);
await outStream.FinishAsync(System.Threading.CancellationToken.None);
outStream.Dispose();

#endif
ms.Seek(0, SeekOrigin.Begin);

using (var inStream = new GZipInputStream(ms))
{
var readBuffer = new byte[content.Length];
inStream.Read(readBuffer, 0, readBuffer.Length);
Assert.AreEqual(content, Encoding.ASCII.GetString(readBuffer));
Assert.AreEqual("file.ext", inStream.GetFilename());
}
}

/// <summary>
/// Test creating an empty gzip stream using async
/// </summary>
[Test]
[Category("GZip")]
[Category("Async")]
public async Task EmptyGZipStreamAsync()
{
#if NETCOREAPP3_1_OR_GREATER
await using var ms = new MemoryStream();
await using (var outStream = new GZipOutputStream(ms) { IsStreamOwner = false })
{
// No content
}
#else
var ms = new MemoryStream();
var outStream = new GZipOutputStream(ms){ IsStreamOwner = false };
await outStream.FinishAsync(System.Threading.CancellationToken.None);
outStream.Dispose();

#endif
ms.Seek(0, SeekOrigin.Begin);

using (var inStream = new GZipInputStream(ms))
using (var reader = new StreamReader(inStream))
{
var content = await reader.ReadToEndAsync();
Assert.IsEmpty(content);
}
}
}
}
Loading

0 comments on commit b314d3d

Please sign in to comment.