diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index 0b292fb3f..36349c886 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -270,7 +270,76 @@ public void PutNextEntry(ZipEntry entry) WriteOutput(GetEntryEncryptionHeader(entry)); } } - + + /// + /// Starts a new passthrough Zip entry. It automatically closes the previous + /// entry if present. + /// Passthrough entry is an entry that is created from compressed data. + /// It is useful to avoid recompression to save CPU resources if compressed data is already disposable. + /// All entry elements bar name, crc, size and compressed size are optional, but must be correct if present. + /// Compression should be set to Deflated. + /// + /// + /// the entry. + /// + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occurred. + /// + /// + /// if stream was finished. + /// + /// + /// Crc is not set
+ /// Size is not set
+ /// CompressedSize is not set
+ /// CompressionMethod is not Deflate
+ /// Too many entries in the Zip file
+ /// Entry name is too long
+ /// Finish has already been called
+ ///
+ /// + /// The Compression method specified for the entry is unsupported
+ /// Entry is encrypted
+ ///
+ public void PutNextPassthroughEntry(ZipEntry entry) + { + if(curEntry != null) + { + CloseEntry(); + } + + if(entry.Crc < 0) + { + throw new ZipException("Crc must be set for passthrough entry"); + } + + if(entry.Size < 0) + { + throw new ZipException("Size must be set for passthrough entry"); + } + + if(entry.CompressedSize < 0) + { + throw new ZipException("CompressedSize must be set for passthrough entry"); + } + + if(entry.CompressionMethod != CompressionMethod.Deflated) + { + throw new NotImplementedException("Only Deflated entries are supported for passthrough"); + } + + if(!string.IsNullOrEmpty(Password)) + { + throw new NotImplementedException("Encrypted passthrough entries are not supported"); + } + + PutNextEntry(baseOutputStream_, entry, 0, true); + } + + private void WriteOutput(byte[] bytes) => baseOutputStream_.Write(bytes, 0, bytes.Length); @@ -282,7 +351,7 @@ private byte[] GetEntryEncryptionHeader(ZipEntry entry) => ? InitializeAESPassword(entry, Password) : CreateZipCryptoHeader(entry.Crc < 0 ? entry.DosTime << 16 : entry.Crc); - internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) + internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0, bool passthroughEntry = false) { if (entry == null) { @@ -313,6 +382,8 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) throw new InvalidOperationException("The Password property must be set before AES encrypted entries can be added"); } + entryIsPassthrough = passthroughEntry; + int compressionLevel = defaultCompressionLevel; // Clear flags that the library manages internally @@ -322,7 +393,7 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) bool headerInfoAvailable; // No need to compress - definitely no data. - if (entry.Size == 0) + if (entry.Size == 0 && !entryIsPassthrough) { entry.CompressedSize = entry.Size; entry.Crc = 0; @@ -406,14 +477,17 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) // Activate the entry. curEntry = entry; + size = 0; + + if(entryIsPassthrough) + return; + crc.Reset(); if (method == CompressionMethod.Deflated) { deflater_.Reset(); deflater_.SetLevel(compressionLevel); } - size = 0; - } /// @@ -506,6 +580,17 @@ internal void WriteEntryFooter(Stream stream) throw new InvalidOperationException("No open entry"); } + if(entryIsPassthrough) + { + if(curEntry.CompressedSize != size) + { + throw new ZipException($"compressed size was {size}, but {curEntry.CompressedSize} expected"); + } + + offset += size; + return; + } + long csize = size; // First finish the deflater, if appropriate @@ -695,30 +780,28 @@ public override void Write(byte[] buffer, int offset, int count) throw new ArgumentException("Invalid offset/count combination"); } - if (curEntry.AESKeySize == 0) + if (curEntry.AESKeySize == 0 && !entryIsPassthrough) { - // Only update CRC if AES is not enabled + // Only update CRC if AES is not enabled and entry is not a passthrough one crc.Update(new ArraySegment(buffer, offset, count)); } size += count; - switch (curMethod) + if(curMethod == CompressionMethod.Stored || entryIsPassthrough) { - case CompressionMethod.Deflated: - base.Write(buffer, offset, count); - break; - - case CompressionMethod.Stored: - if (Password != null) - { - CopyAndEncrypt(buffer, offset, count); - } - else - { - baseOutputStream_.Write(buffer, offset, count); - } - break; + if (Password != null) + { + CopyAndEncrypt(buffer, offset, count); + } + else + { + baseOutputStream_.Write(buffer, offset, count); + } + } + else + { + base.Write(buffer, offset, count); } } @@ -844,6 +927,8 @@ public override void Flush() /// private ZipEntry curEntry; + private bool entryIsPassthrough; + private int defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION; private CompressionMethod curMethod = CompressionMethod.Deflated; diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/GeneralHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/GeneralHandling.cs index ad97563aa..e16a82967 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/GeneralHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/GeneralHandling.cs @@ -1,5 +1,7 @@ -using ICSharpCode.SharpZipLib.Tests.TestSupport; +using ICSharpCode.SharpZipLib.Checksum; +using ICSharpCode.SharpZipLib.Tests.TestSupport; using ICSharpCode.SharpZipLib.Zip; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using NUnit.Framework; using System; using System.IO; diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/PassthroughTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/PassthroughTests.cs new file mode 100644 index 000000000..1a4b266f2 --- /dev/null +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/PassthroughTests.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; +using ICSharpCode.SharpZipLib.Checksum; +using ICSharpCode.SharpZipLib.Tests.TestSupport; +using ICSharpCode.SharpZipLib.Zip; +using NUnit.Framework; + +namespace ICSharpCode.SharpZipLib.Tests.Zip +{ + [TestFixture] + public class PassthroughTests + { + [Test] + [Category("Zip")] + public void AddingValidPrecompressedEntryToZipOutputStream() + { + using var ms = new MemoryStream(); + + using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false}) + { + var (compressedData, crc, size) = CreateDeflatedData(); + var entry = new ZipEntry("dummyfile.tst") + { + CompressionMethod = CompressionMethod.Deflated, + Size = size, + Crc = (uint)crc.Value, + CompressedSize = compressedData.Length, + }; + + outStream.PutNextPassthroughEntry(entry); + + compressedData.CopyTo(outStream); + } + + Assert.IsTrue(ZipTesting.TestArchive(ms.ToArray())); + } + + private static (MemoryStream, Crc32, int) CreateDeflatedData() + { + var data = Encoding.UTF8.GetBytes("Hello, world"); + + var crc = new Crc32(); + crc.Update(data); + + var compressedData = new MemoryStream(); + using(var gz = new DeflateStream(compressedData, CompressionMode.Compress, leaveOpen: true)) + { + gz.Write(data, 0, data.Length); + } + compressedData.Position = 0; + + return (compressedData, crc, data.Length); + } + + [Test] + [Category("Zip")] + public void AddingPrecompressedEntryToZipOutputStreamWithInvalidSize() + { + using var outStream = new ZipOutputStream(new MemoryStream()); + var (compressedData, crc, size) = CreateDeflatedData(); + outStream.Password = "mockpassword"; + var entry = new ZipEntry("dummyfile.tst") + { + CompressionMethod = CompressionMethod.Stored, + Crc = (uint)crc.Value, + CompressedSize = compressedData.Length, + }; + + Assert.Throws(() => + { + outStream.PutNextPassthroughEntry(entry); + }); + } + + + [Test] + [Category("Zip")] + public void AddingPrecompressedEntryToZipOutputStreamWithInvalidCompressedSize() + { + using var outStream = new ZipOutputStream(new MemoryStream()); + var (compressedData, crc, size) = CreateDeflatedData(); + outStream.Password = "mockpassword"; + var entry = new ZipEntry("dummyfile.tst") + { + CompressionMethod = CompressionMethod.Stored, + Size = size, + Crc = (uint)crc.Value, + }; + + Assert.Throws(() => + { + outStream.PutNextPassthroughEntry(entry); + }); + } + + [Test] + [Category("Zip")] + public void AddingPrecompressedEntryToZipOutputStreamWithNonSupportedMethod() + { + using var outStream = new ZipOutputStream(new MemoryStream()); + var (compressedData, crc, size) = CreateDeflatedData(); + outStream.Password = "mockpassword"; + var entry = new ZipEntry("dummyfile.tst") + { + CompressionMethod = CompressionMethod.LZMA, + Size = size, + Crc = (uint)crc.Value, + CompressedSize = compressedData.Length, + }; + + Assert.Throws(() => + { + outStream.PutNextPassthroughEntry(entry); + }); + } + + [Test] + [Category("Zip")] + public void AddingPrecompressedEntryToZipOutputStreamWithEncryption() + { + using var outStream = new ZipOutputStream(new MemoryStream()); + var (compressedData, crc, size) = CreateDeflatedData(); + outStream.Password = "mockpassword"; + var entry = new ZipEntry("dummyfile.tst") + { + CompressionMethod = CompressionMethod.Deflated, + Size = size, + Crc = (uint)crc.Value, + CompressedSize = compressedData.Length, + }; + + Assert.Throws(() => + { + outStream.PutNextPassthroughEntry(entry); + }); + } + } +}