Skip to content

Commit

Permalink
feat(gzip): add GzipOutputStream async support
Browse files Browse the repository at this point in the history
  • Loading branch information
piksel committed Oct 9, 2021
1 parent e1e1a91 commit e63c41f
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 48 deletions.
184 changes: 136 additions & 48 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 @@ -162,6 +164,26 @@ public override void Write(byte[] buffer, int offset, int count)
base.Write(buffer, offset, count);
}

#if NETSTANDARD2_1_OR_GREATER
/// <inheritdoc cref="DeflaterOutputStream.WriteAsync(byte[],int,int,CancellationToken)"/>
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<byte>(buffer, offset, count));
await base.WriteAsync(buffer, offset, count, cancellationToken);
}
#endif

/// <summary>
/// Writes remaining compressed output data to the output stream
/// and closes it.
Expand All @@ -184,6 +206,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,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
/// <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();
}

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
}
Expand Down
45 changes: 45 additions & 0 deletions test/ICSharpCode.SharpZipLib.Tests/GZip/GZipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ICSharpCode.SharpZipLib.Tests.GZip
{
Expand Down Expand Up @@ -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

/// <summary>
/// Should gracefully handle reading from a stream that becomes unreadable after
/// all of the data has been read.
Expand Down

0 comments on commit e63c41f

Please sign in to comment.