Skip to content

Commit b314d3d

Browse files
authored
feat(gzip): add GzipOutputStream async support (#672)
1 parent 519ed73 commit b314d3d

File tree

5 files changed

+279
-71
lines changed

5 files changed

+279
-71
lines changed

src/ICSharpCode.SharpZipLib/GZip/GzipOutputStream.cs

Lines changed: 121 additions & 50 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
{
@@ -184,6 +186,30 @@ protected override void Dispose(bool disposing)
184186
}
185187
}
186188
}
189+
190+
#if NETSTANDARD2_1_OR_GREATER
191+
/// <inheritdoc cref="DeflaterOutputStream.DisposeAsync"/>
192+
public override async ValueTask DisposeAsync()
193+
{
194+
try
195+
{
196+
await FinishAsync(CancellationToken.None);
197+
}
198+
finally
199+
{
200+
if (state_ != OutputState.Closed)
201+
{
202+
state_ = OutputState.Closed;
203+
if (IsStreamOwner)
204+
{
205+
await baseOutputStream_.DisposeAsync();
206+
}
207+
}
208+
209+
await base.DisposeAsync();
210+
}
211+
}
212+
#endif
187213

188214
/// <summary>
189215
/// Flushes the stream by ensuring the header is written, and then calling <see cref="DeflaterOutputStream.Flush">Flush</see>
@@ -218,74 +244,119 @@ public override void Finish()
218244
{
219245
state_ = OutputState.Finished;
220246
base.Finish();
221-
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),
232-
233-
(byte) totalin, (byte) (totalin >> 8),
234-
(byte) (totalin >> 16), (byte) (totalin >> 24)
235-
};
236-
}
237-
247+
var gzipFooter = GetFooter();
238248
baseOutputStream_.Write(gzipFooter, 0, gzipFooter.Length);
239249
}
240250
}
251+
252+
/// <inheritdoc cref="Flush"/>
253+
public override async Task FlushAsync(CancellationToken ct)
254+
{
255+
await WriteHeaderAsync();
256+
await base.FlushAsync(ct);
257+
}
258+
259+
260+
/// <inheritdoc cref="Finish"/>
261+
public override async Task FinishAsync(CancellationToken ct)
262+
{
263+
// If no data has been written a header should be added.
264+
if (state_ == OutputState.Header)
265+
{
266+
await WriteHeaderAsync();
267+
}
268+
269+
if (state_ == OutputState.Footer)
270+
{
271+
state_ = OutputState.Finished;
272+
await base.FinishAsync(ct);
273+
var gzipFooter = GetFooter();
274+
await baseOutputStream_.WriteAsync(gzipFooter, 0, gzipFooter.Length, ct);
275+
}
276+
}
241277

242278
#endregion DeflaterOutputStream overrides
243279

244280
#region Support Routines
245281

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

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,
287+
byte[] gzipFooter;
260288

261-
// The compression type
262-
GZipConstants.CompressionMethodDeflate,
289+
unchecked
290+
{
291+
gzipFooter = new [] {
292+
(byte) crcval,
293+
(byte) (crcval >> 8),
294+
(byte) (crcval >> 16),
295+
(byte) (crcval >> 24),
296+
(byte) totalin,
297+
(byte) (totalin >> 8),
298+
(byte) (totalin >> 16),
299+
(byte) (totalin >> 24),
300+
};
301+
}
263302

264-
// The flags (not set)
265-
(byte)flags,
303+
return gzipFooter;
304+
}
266305

267-
// The modification time
268-
(byte) mod_time, (byte) (mod_time >> 8),
269-
(byte) (mod_time >> 16), (byte) (mod_time >> 24),
306+
private byte[] GetHeader()
307+
{
308+
var modTime = (int)((DateTime.Now.Ticks - new DateTime(1970, 1, 1).Ticks) / 10000000L); // Ticks give back 100ns intervals
309+
byte[] gzipHeader = {
310+
// The two magic bytes
311+
GZipConstants.ID1,
312+
GZipConstants.ID2,
270313

271-
// The extra flags
272-
0,
314+
// The compression type
315+
GZipConstants.CompressionMethodDeflate,
273316

274-
// The OS type (unknown)
275-
255
276-
};
317+
// The flags (not set)
318+
(byte)flags,
277319

278-
baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length);
320+
// The modification time
321+
(byte) modTime, (byte) (modTime >> 8),
322+
(byte) (modTime >> 16), (byte) (modTime >> 24),
279323

280-
if (flags.HasFlag(GZipFlags.FNAME))
281-
{
282-
var fname = GZipConstants.Encoding.GetBytes(fileName);
283-
baseOutputStream_.Write(fname, 0, fname.Length);
324+
// The extra flags
325+
0,
284326

285-
// End filename string with a \0
286-
baseOutputStream_.Write(new byte[] { 0 }, 0, 1);
287-
}
327+
// The OS type (unknown)
328+
255
329+
};
330+
331+
if (!flags.HasFlag(GZipFlags.FNAME))
332+
{
333+
return gzipHeader;
288334
}
335+
336+
337+
return gzipHeader
338+
.Concat(GZipConstants.Encoding.GetBytes(fileName))
339+
.Concat(new byte []{0}) // End filename string with a \0
340+
.ToArray();
341+
}
342+
343+
private static string CleanFilename(string path)
344+
=> path.Substring(path.LastIndexOf('/') + 1);
345+
346+
private void WriteHeader()
347+
{
348+
if (state_ != OutputState.Header) return;
349+
state_ = OutputState.Footer;
350+
var gzipHeader = GetHeader();
351+
baseOutputStream_.Write(gzipHeader, 0, gzipHeader.Length);
352+
}
353+
354+
private async Task WriteHeaderAsync()
355+
{
356+
if (state_ != OutputState.Header) return;
357+
state_ = OutputState.Footer;
358+
var gzipHeader = GetHeader();
359+
await baseOutputStream_.WriteAsync(gzipHeader, 0, gzipHeader.Length);
289360
}
290361

291362
#endregion Support Routines

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ protected override void Dispose(bool disposing)
412412
}
413413
}
414414

415-
#if NETSTANDARD2_1
415+
#if NETSTANDARD2_1_OR_GREATER
416416
/// <summary>
417417
/// Calls <see cref="FinishAsync"/> and closes the underlying
418418
/// stream when <see cref="IsStreamOwner"></see> is true.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System.IO;
2+
using System.Text;
3+
using System.Threading.Tasks;
4+
using ICSharpCode.SharpZipLib.GZip;
5+
using ICSharpCode.SharpZipLib.Tests.TestSupport;
6+
using NUnit.Framework;
7+
8+
namespace ICSharpCode.SharpZipLib.Tests.GZip
9+
{
10+
11+
12+
[TestFixture]
13+
public class GZipAsyncTests
14+
{
15+
[Test]
16+
[Category("GZip")]
17+
[Category("Async")]
18+
public async Task SmallBufferDecompressionAsync([Values(0, 1, 3)] int seed)
19+
{
20+
var outputBufferSize = 100000;
21+
var outputBuffer = new byte[outputBufferSize];
22+
var inputBuffer = Utils.GetDummyBytes(outputBufferSize * 4, seed);
23+
24+
#if NETCOREAPP3_1_OR_GREATER
25+
await using var msGzip = new MemoryStream();
26+
await using (var gzos = new GZipOutputStream(msGzip){IsStreamOwner = false})
27+
{
28+
await gzos.WriteAsync(inputBuffer, 0, inputBuffer.Length);
29+
}
30+
31+
msGzip.Seek(0, SeekOrigin.Begin);
32+
33+
using (var gzis = new GZipInputStream(msGzip))
34+
await using (var msRaw = new MemoryStream())
35+
{
36+
int readOut;
37+
while ((readOut = gzis.Read(outputBuffer, 0, outputBuffer.Length)) > 0)
38+
{
39+
await msRaw.WriteAsync(outputBuffer, 0, readOut);
40+
}
41+
42+
var resultBuffer = msRaw.ToArray();
43+
for (var i = 0; i < resultBuffer.Length; i++)
44+
{
45+
Assert.AreEqual(inputBuffer[i], resultBuffer[i]);
46+
}
47+
}
48+
#else
49+
using var msGzip = new MemoryStream();
50+
using (var gzos = new GZipOutputStream(msGzip){IsStreamOwner = false})
51+
{
52+
await gzos.WriteAsync(inputBuffer, 0, inputBuffer.Length);
53+
}
54+
55+
msGzip.Seek(0, SeekOrigin.Begin);
56+
57+
using (var gzis = new GZipInputStream(msGzip))
58+
using (var msRaw = new MemoryStream())
59+
{
60+
int readOut;
61+
while ((readOut = gzis.Read(outputBuffer, 0, outputBuffer.Length)) > 0)
62+
{
63+
await msRaw.WriteAsync(outputBuffer, 0, readOut);
64+
}
65+
66+
var resultBuffer = msRaw.ToArray();
67+
for (var i = 0; i < resultBuffer.Length; i++)
68+
{
69+
Assert.AreEqual(inputBuffer[i], resultBuffer[i]);
70+
}
71+
}
72+
#endif
73+
}
74+
75+
/// <summary>
76+
/// Basic compress/decompress test
77+
/// </summary>
78+
[Test]
79+
[Category("GZip")]
80+
[Category("Async")]
81+
public async Task OriginalFilenameAsync()
82+
{
83+
var content = "FileContents";
84+
85+
#if NETCOREAPP3_1_OR_GREATER
86+
await using var ms = new MemoryStream();
87+
await using (var outStream = new GZipOutputStream(ms) { IsStreamOwner = false })
88+
{
89+
outStream.FileName = "/path/to/file.ext";
90+
outStream.Write(Encoding.ASCII.GetBytes(content));
91+
}
92+
#else
93+
var ms = new MemoryStream();
94+
var outStream = new GZipOutputStream(ms){ IsStreamOwner = false };
95+
outStream.FileName = "/path/to/file.ext";
96+
var bytes = Encoding.ASCII.GetBytes(content);
97+
outStream.Write(bytes, 0, bytes.Length);
98+
await outStream.FinishAsync(System.Threading.CancellationToken.None);
99+
outStream.Dispose();
100+
101+
#endif
102+
ms.Seek(0, SeekOrigin.Begin);
103+
104+
using (var inStream = new GZipInputStream(ms))
105+
{
106+
var readBuffer = new byte[content.Length];
107+
inStream.Read(readBuffer, 0, readBuffer.Length);
108+
Assert.AreEqual(content, Encoding.ASCII.GetString(readBuffer));
109+
Assert.AreEqual("file.ext", inStream.GetFilename());
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Test creating an empty gzip stream using async
115+
/// </summary>
116+
[Test]
117+
[Category("GZip")]
118+
[Category("Async")]
119+
public async Task EmptyGZipStreamAsync()
120+
{
121+
#if NETCOREAPP3_1_OR_GREATER
122+
await using var ms = new MemoryStream();
123+
await using (var outStream = new GZipOutputStream(ms) { IsStreamOwner = false })
124+
{
125+
// No content
126+
}
127+
#else
128+
var ms = new MemoryStream();
129+
var outStream = new GZipOutputStream(ms){ IsStreamOwner = false };
130+
await outStream.FinishAsync(System.Threading.CancellationToken.None);
131+
outStream.Dispose();
132+
133+
#endif
134+
ms.Seek(0, SeekOrigin.Begin);
135+
136+
using (var inStream = new GZipInputStream(ms))
137+
using (var reader = new StreamReader(inStream))
138+
{
139+
var content = await reader.ReadToEndAsync();
140+
Assert.IsEmpty(content);
141+
}
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)