Skip to content

Commit ff64d0a

Browse files
Temtaimepiksel
andauthored
feat(zip): enable ZipOuputStream to write precompressed files (#683)
Co-authored-by: nils måsén <nils@piksel.se>
1 parent 612969e commit ff64d0a

File tree

3 files changed

+250
-23
lines changed

3 files changed

+250
-23
lines changed

src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs

+107-22
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,76 @@ public void PutNextEntry(ZipEntry entry)
270270
WriteOutput(GetEntryEncryptionHeader(entry));
271271
}
272272
}
273-
273+
274+
/// <summary>
275+
/// Starts a new passthrough Zip entry. It automatically closes the previous
276+
/// entry if present.
277+
/// Passthrough entry is an entry that is created from compressed data.
278+
/// It is useful to avoid recompression to save CPU resources if compressed data is already disposable.
279+
/// All entry elements bar name, crc, size and compressed size are optional, but must be correct if present.
280+
/// Compression should be set to Deflated.
281+
/// </summary>
282+
/// <param name="entry">
283+
/// the entry.
284+
/// </param>
285+
/// <exception cref="System.ArgumentNullException">
286+
/// if entry passed is null.
287+
/// </exception>
288+
/// <exception cref="System.IO.IOException">
289+
/// if an I/O error occurred.
290+
/// </exception>
291+
/// <exception cref="System.InvalidOperationException">
292+
/// if stream was finished.
293+
/// </exception>
294+
/// <exception cref="ZipException">
295+
/// Crc is not set<br/>
296+
/// Size is not set<br/>
297+
/// CompressedSize is not set<br/>
298+
/// CompressionMethod is not Deflate<br/>
299+
/// Too many entries in the Zip file<br/>
300+
/// Entry name is too long<br/>
301+
/// Finish has already been called<br/>
302+
/// </exception>
303+
/// <exception cref="System.NotImplementedException">
304+
/// The Compression method specified for the entry is unsupported<br/>
305+
/// Entry is encrypted<br/>
306+
/// </exception>
307+
public void PutNextPassthroughEntry(ZipEntry entry)
308+
{
309+
if(curEntry != null)
310+
{
311+
CloseEntry();
312+
}
313+
314+
if(entry.Crc < 0)
315+
{
316+
throw new ZipException("Crc must be set for passthrough entry");
317+
}
318+
319+
if(entry.Size < 0)
320+
{
321+
throw new ZipException("Size must be set for passthrough entry");
322+
}
323+
324+
if(entry.CompressedSize < 0)
325+
{
326+
throw new ZipException("CompressedSize must be set for passthrough entry");
327+
}
328+
329+
if(entry.CompressionMethod != CompressionMethod.Deflated)
330+
{
331+
throw new NotImplementedException("Only Deflated entries are supported for passthrough");
332+
}
333+
334+
if(!string.IsNullOrEmpty(Password))
335+
{
336+
throw new NotImplementedException("Encrypted passthrough entries are not supported");
337+
}
338+
339+
PutNextEntry(baseOutputStream_, entry, 0, true);
340+
}
341+
342+
274343
private void WriteOutput(byte[] bytes)
275344
=> baseOutputStream_.Write(bytes, 0, bytes.Length);
276345

@@ -282,7 +351,7 @@ private byte[] GetEntryEncryptionHeader(ZipEntry entry) =>
282351
? InitializeAESPassword(entry, Password)
283352
: CreateZipCryptoHeader(entry.Crc < 0 ? entry.DosTime << 16 : entry.Crc);
284353

285-
internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0)
354+
internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0, bool passthroughEntry = false)
286355
{
287356
if (entry == null)
288357
{
@@ -313,6 +382,8 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0)
313382
throw new InvalidOperationException("The Password property must be set before AES encrypted entries can be added");
314383
}
315384

385+
entryIsPassthrough = passthroughEntry;
386+
316387
int compressionLevel = defaultCompressionLevel;
317388

318389
// Clear flags that the library manages internally
@@ -322,7 +393,7 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0)
322393
bool headerInfoAvailable;
323394

324395
// No need to compress - definitely no data.
325-
if (entry.Size == 0)
396+
if (entry.Size == 0 && !entryIsPassthrough)
326397
{
327398
entry.CompressedSize = entry.Size;
328399
entry.Crc = 0;
@@ -406,14 +477,17 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0)
406477

407478
// Activate the entry.
408479
curEntry = entry;
480+
size = 0;
481+
482+
if(entryIsPassthrough)
483+
return;
484+
409485
crc.Reset();
410486
if (method == CompressionMethod.Deflated)
411487
{
412488
deflater_.Reset();
413489
deflater_.SetLevel(compressionLevel);
414490
}
415-
size = 0;
416-
417491
}
418492

419493
/// <summary>
@@ -506,6 +580,17 @@ internal void WriteEntryFooter(Stream stream)
506580
throw new InvalidOperationException("No open entry");
507581
}
508582

583+
if(entryIsPassthrough)
584+
{
585+
if(curEntry.CompressedSize != size)
586+
{
587+
throw new ZipException($"compressed size was {size}, but {curEntry.CompressedSize} expected");
588+
}
589+
590+
offset += size;
591+
return;
592+
}
593+
509594
long csize = size;
510595

511596
// First finish the deflater, if appropriate
@@ -695,30 +780,28 @@ public override void Write(byte[] buffer, int offset, int count)
695780
throw new ArgumentException("Invalid offset/count combination");
696781
}
697782

698-
if (curEntry.AESKeySize == 0)
783+
if (curEntry.AESKeySize == 0 && !entryIsPassthrough)
699784
{
700-
// Only update CRC if AES is not enabled
785+
// Only update CRC if AES is not enabled and entry is not a passthrough one
701786
crc.Update(new ArraySegment<byte>(buffer, offset, count));
702787
}
703788

704789
size += count;
705790

706-
switch (curMethod)
791+
if(curMethod == CompressionMethod.Stored || entryIsPassthrough)
707792
{
708-
case CompressionMethod.Deflated:
709-
base.Write(buffer, offset, count);
710-
break;
711-
712-
case CompressionMethod.Stored:
713-
if (Password != null)
714-
{
715-
CopyAndEncrypt(buffer, offset, count);
716-
}
717-
else
718-
{
719-
baseOutputStream_.Write(buffer, offset, count);
720-
}
721-
break;
793+
if (Password != null)
794+
{
795+
CopyAndEncrypt(buffer, offset, count);
796+
}
797+
else
798+
{
799+
baseOutputStream_.Write(buffer, offset, count);
800+
}
801+
}
802+
else
803+
{
804+
base.Write(buffer, offset, count);
722805
}
723806
}
724807

@@ -844,6 +927,8 @@ public override void Flush()
844927
/// </summary>
845928
private ZipEntry curEntry;
846929

930+
private bool entryIsPassthrough;
931+
847932
private int defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION;
848933

849934
private CompressionMethod curMethod = CompressionMethod.Deflated;

test/ICSharpCode.SharpZipLib.Tests/Zip/GeneralHandling.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using ICSharpCode.SharpZipLib.Tests.TestSupport;
1+
using ICSharpCode.SharpZipLib.Checksum;
2+
using ICSharpCode.SharpZipLib.Tests.TestSupport;
23
using ICSharpCode.SharpZipLib.Zip;
4+
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
35
using NUnit.Framework;
46
using System;
57
using System.IO;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Text;
5+
using ICSharpCode.SharpZipLib.Checksum;
6+
using ICSharpCode.SharpZipLib.Tests.TestSupport;
7+
using ICSharpCode.SharpZipLib.Zip;
8+
using NUnit.Framework;
9+
10+
namespace ICSharpCode.SharpZipLib.Tests.Zip
11+
{
12+
[TestFixture]
13+
public class PassthroughTests
14+
{
15+
[Test]
16+
[Category("Zip")]
17+
public void AddingValidPrecompressedEntryToZipOutputStream()
18+
{
19+
using var ms = new MemoryStream();
20+
21+
using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false})
22+
{
23+
var (compressedData, crc, size) = CreateDeflatedData();
24+
var entry = new ZipEntry("dummyfile.tst")
25+
{
26+
CompressionMethod = CompressionMethod.Deflated,
27+
Size = size,
28+
Crc = (uint)crc.Value,
29+
CompressedSize = compressedData.Length,
30+
};
31+
32+
outStream.PutNextPassthroughEntry(entry);
33+
34+
compressedData.CopyTo(outStream);
35+
}
36+
37+
Assert.IsTrue(ZipTesting.TestArchive(ms.ToArray()));
38+
}
39+
40+
private static (MemoryStream, Crc32, int) CreateDeflatedData()
41+
{
42+
var data = Encoding.UTF8.GetBytes("Hello, world");
43+
44+
var crc = new Crc32();
45+
crc.Update(data);
46+
47+
var compressedData = new MemoryStream();
48+
using(var gz = new DeflateStream(compressedData, CompressionMode.Compress, leaveOpen: true))
49+
{
50+
gz.Write(data, 0, data.Length);
51+
}
52+
compressedData.Position = 0;
53+
54+
return (compressedData, crc, data.Length);
55+
}
56+
57+
[Test]
58+
[Category("Zip")]
59+
public void AddingPrecompressedEntryToZipOutputStreamWithInvalidSize()
60+
{
61+
using var outStream = new ZipOutputStream(new MemoryStream());
62+
var (compressedData, crc, size) = CreateDeflatedData();
63+
outStream.Password = "mockpassword";
64+
var entry = new ZipEntry("dummyfile.tst")
65+
{
66+
CompressionMethod = CompressionMethod.Stored,
67+
Crc = (uint)crc.Value,
68+
CompressedSize = compressedData.Length,
69+
};
70+
71+
Assert.Throws<ZipException>(() =>
72+
{
73+
outStream.PutNextPassthroughEntry(entry);
74+
});
75+
}
76+
77+
78+
[Test]
79+
[Category("Zip")]
80+
public void AddingPrecompressedEntryToZipOutputStreamWithInvalidCompressedSize()
81+
{
82+
using var outStream = new ZipOutputStream(new MemoryStream());
83+
var (compressedData, crc, size) = CreateDeflatedData();
84+
outStream.Password = "mockpassword";
85+
var entry = new ZipEntry("dummyfile.tst")
86+
{
87+
CompressionMethod = CompressionMethod.Stored,
88+
Size = size,
89+
Crc = (uint)crc.Value,
90+
};
91+
92+
Assert.Throws<ZipException>(() =>
93+
{
94+
outStream.PutNextPassthroughEntry(entry);
95+
});
96+
}
97+
98+
[Test]
99+
[Category("Zip")]
100+
public void AddingPrecompressedEntryToZipOutputStreamWithNonSupportedMethod()
101+
{
102+
using var outStream = new ZipOutputStream(new MemoryStream());
103+
var (compressedData, crc, size) = CreateDeflatedData();
104+
outStream.Password = "mockpassword";
105+
var entry = new ZipEntry("dummyfile.tst")
106+
{
107+
CompressionMethod = CompressionMethod.LZMA,
108+
Size = size,
109+
Crc = (uint)crc.Value,
110+
CompressedSize = compressedData.Length,
111+
};
112+
113+
Assert.Throws<NotImplementedException>(() =>
114+
{
115+
outStream.PutNextPassthroughEntry(entry);
116+
});
117+
}
118+
119+
[Test]
120+
[Category("Zip")]
121+
public void AddingPrecompressedEntryToZipOutputStreamWithEncryption()
122+
{
123+
using var outStream = new ZipOutputStream(new MemoryStream());
124+
var (compressedData, crc, size) = CreateDeflatedData();
125+
outStream.Password = "mockpassword";
126+
var entry = new ZipEntry("dummyfile.tst")
127+
{
128+
CompressionMethod = CompressionMethod.Deflated,
129+
Size = size,
130+
Crc = (uint)crc.Value,
131+
CompressedSize = compressedData.Length,
132+
};
133+
134+
Assert.Throws<NotImplementedException>(() =>
135+
{
136+
outStream.PutNextPassthroughEntry(entry);
137+
});
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)