From ba3b30f7a66a9c25035d52ee05d9f4d536e51ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 7 Dec 2020 16:26:43 +0100 Subject: [PATCH 1/7] Add async support on ZipOutputStream This is a very rough first pass at adding async support to ZipOutputStream. All method doing synchronous stream writes have been duplicated with an asynchronous counterpart, starting from `ZipOutputStream.PutNextEntryAsync()`. Unfortunately, this makes a lot of duplicated code but is unavoidable to have both sync + async code paths. .NET Standard 2.1 has been added to the target frameworks. It's required to get full async support with `Stream.DisposeAsync()`. New unit tests must be written to cover the new async code paths. Fixes #223 --- .../ICSharpCode.SharpZipLib.csproj | 4 +- .../Streams/DeflaterOutputStream.cs | 78 ++ .../Zip/ZipHelperStream.cs | 172 ++++ .../Zip/ZipOutputStream.cs | 750 +++++++++++++++++- 4 files changed, 962 insertions(+), 42 deletions(-) diff --git a/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj b/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj index 03dcc861d..0bfe98b80 100644 --- a/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj +++ b/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj @@ -1,7 +1,7 @@  - netstandard2;net45 + netstandard2.0;netstandard2.1;net45 True ../../assets/ICSharpCode.SharpZipLib.snk true @@ -40,5 +40,5 @@ Please see https://github.com/icsharpcode/SharpZipLib/wiki/Release-1.3.1 for mor images - + diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs index 03cac7358..f39442285 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Zip.Compression.Streams { @@ -131,6 +133,50 @@ public virtual void Finish() } } + /// + /// Finishes the stream by calling finish() on the deflater. + /// + /// The that can be used to cancel the operation. + /// + /// Not all input is deflated + /// + public virtual async Task FinishAsync(CancellationToken cancellationToken) + { + deflater_.Finish(); + while (!deflater_.IsFinished) + { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) + { + break; + } + + if (cryptoTransform_ != null) + { + EncryptBlock(buffer_, 0, len); + } + + await baseOutputStream_.WriteAsync(buffer_, 0, len, cancellationToken); + } + + if (!deflater_.IsFinished) + { + throw new SharpZipBaseException("Can't deflate all input?"); + } + + await baseOutputStream_.FlushAsync(cancellationToken); + + if (cryptoTransform_ != null) + { + if (cryptoTransform_ is ZipAESTransform) + { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } + } + /// /// Gets or sets a flag indicating ownership of underlying stream. /// When the flag is true will close the underlying stream also. @@ -419,6 +465,38 @@ protected override void Dispose(bool disposing) } } +#if NETSTANDARD2_1 + /// + /// Calls and closes the underlying + /// stream when is true. + /// + public override async ValueTask DisposeAsync() + { + if (!isClosed_) + { + isClosed_ = true; + + try + { + await FinishAsync(CancellationToken.None); + if (cryptoTransform_ != null) + { + GetAuthCodeIfAES(); + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } + } + finally + { + if (IsStreamOwner) + { + await baseOutputStream_.DisposeAsync(); + } + } + } + } +#endif + /// /// Get the Auth code for AES encrypted entries /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs index dd7d25d94..f9c277ff2 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Zip { @@ -361,6 +363,41 @@ public void WriteZip64EndOfCentralDirectory(long noOfEntries, long sizeEntries, WriteLEInt(1); } + /// + /// Write Zip64 end of central directory records (File header and locator). + /// + /// The number of entries in the central directory. + /// The size of entries in the central directory. + /// The offset of the central directory. + /// The that can be used to cancel the operation. + public async Task WriteZip64EndOfCentralDirectoryAsync(long noOfEntries, long sizeEntries, long centralDirOffset, CancellationToken cancellationToken) + { + long centralSignatureOffset = centralDirOffset + sizeEntries; + await WriteLEIntAsync(ZipConstants.Zip64CentralFileHeaderSignature, cancellationToken); + await WriteLELongAsync(44, cancellationToken); // Size of this record (total size of remaining fields in header or full size - 12) + await WriteLEShortAsync(ZipConstants.VersionMadeBy, cancellationToken); // Version made by + await WriteLEShortAsync(ZipConstants.VersionZip64, cancellationToken); // Version to extract + await WriteLEIntAsync(0, cancellationToken); // Number of this disk + await WriteLEIntAsync(0, cancellationToken); // number of the disk with the start of the central directory + await WriteLELongAsync(noOfEntries, cancellationToken); // No of entries on this disk + await WriteLELongAsync(noOfEntries, cancellationToken); // Total No of entries in central directory + await WriteLELongAsync(sizeEntries, cancellationToken); // Size of the central directory + await WriteLELongAsync(centralDirOffset, cancellationToken); // offset of start of central directory + // zip64 extensible data sector not catered for here (variable size) + + // Write the Zip64 end of central directory locator + await WriteLEIntAsync(ZipConstants.Zip64CentralDirLocatorSignature, cancellationToken); + + // no of the disk with the start of the zip64 end of central directory + await WriteLEIntAsync(0, cancellationToken); + + // relative offset of the zip64 end of central directory record + await WriteLELongAsync(centralSignatureOffset, cancellationToken); + + // total number of disks + await WriteLEIntAsync(1, cancellationToken); + } + /// /// Write the required records to end the central directory. /// @@ -431,6 +468,77 @@ public void WriteEndOfCentralDirectory(long noOfEntries, long sizeEntries, } } + /// + /// Write the required records to end the central directory. + /// + /// The number of entries in the directory. + /// The size of the entries in the directory. + /// The start of the central directory. + /// The archive comment. (This can be null). + /// The that can be used to cancel the operation. + public async Task WriteEndOfCentralDirectoryAsync(long noOfEntries, long sizeEntries, + long startOfCentralDirectory, byte[] comment, CancellationToken cancellationToken) + { + if ((noOfEntries >= 0xffff) || + (startOfCentralDirectory >= 0xffffffff) || + (sizeEntries >= 0xffffffff)) + { + await WriteZip64EndOfCentralDirectoryAsync(noOfEntries, sizeEntries, startOfCentralDirectory, cancellationToken); + } + + await WriteLEIntAsync(ZipConstants.EndOfCentralDirectorySignature, cancellationToken); + + // TODO: ZipFile Multi disk handling not done + await WriteLEShortAsync(0, cancellationToken); // number of this disk + await WriteLEShortAsync(0, cancellationToken); // no of disk with start of central dir + + // Number of entries + if (noOfEntries >= 0xffff) + { + await WriteLEUshortAsync(0xffff, cancellationToken); // Zip64 marker + await WriteLEUshortAsync(0xffff, cancellationToken); + } + else + { + await WriteLEShortAsync((short)noOfEntries, cancellationToken); // entries in central dir for this disk + await WriteLEShortAsync((short)noOfEntries, cancellationToken); // total entries in central directory + } + + // Size of the central directory + if (sizeEntries >= 0xffffffff) + { + await WriteLEUintAsync(0xffffffff, cancellationToken); // Zip64 marker + } + else + { + await WriteLEIntAsync((int)sizeEntries, cancellationToken); + } + + // offset of start of central directory + if (startOfCentralDirectory >= 0xffffffff) + { + await WriteLEUintAsync(0xffffffff, cancellationToken); // Zip64 marker + } + else + { + await WriteLEIntAsync((int)startOfCentralDirectory, cancellationToken); + } + + int commentLength = (comment != null) ? comment.Length : 0; + + if (commentLength > 0xffff) + { + throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength)); + } + + await WriteLEShortAsync(commentLength, cancellationToken); + + if (commentLength > 0) + { + await WriteAsync(comment, 0, comment.Length, cancellationToken); + } + } + #region LE value reading/writing /// @@ -495,6 +603,16 @@ public void WriteLEShort(int value) stream_.WriteByte((byte)((value >> 8) & 0xff)); } + /// + /// Write an unsigned short in little endian byte order. + /// + /// The value to write. + /// The that can be used to cancel the operation. + public async Task WriteLEShortAsync(int value, CancellationToken cancellationToken) + { + await stream_.WriteAsync(new[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff) }, 0, 2, cancellationToken); + } + /// /// Write a ushort in little endian byte order. /// @@ -505,6 +623,16 @@ public void WriteLEUshort(ushort value) stream_.WriteByte((byte)(value >> 8)); } + /// + /// Write a ushort in little endian byte order. + /// + /// The value to write. + /// The that can be used to cancel the operation. + public async Task WriteLEUshortAsync(ushort value, CancellationToken cancellationToken) + { + await stream_.WriteAsync(new[] { (byte)(value & 0xff), (byte)(value >> 8) }, 0, 2, cancellationToken); + } + /// /// Write an int in little endian byte order. /// @@ -515,6 +643,17 @@ public void WriteLEInt(int value) WriteLEShort(value >> 16); } + /// + /// Write an int in little endian byte order. + /// + /// The value to write. + /// The that can be used to cancel the operation. + public async Task WriteLEIntAsync(int value, CancellationToken cancellationToken) + { + await WriteLEShortAsync(value, cancellationToken); + await WriteLEShortAsync(value >> 16, cancellationToken); + } + /// /// Write a uint in little endian byte order. /// @@ -525,6 +664,17 @@ public void WriteLEUint(uint value) WriteLEUshort((ushort)(value >> 16)); } + /// + /// Write a uint in little endian byte order. + /// + /// The value to write. + /// The that can be used to cancel the operation. + public async Task WriteLEUintAsync(uint value, CancellationToken cancellationToken) + { + await WriteLEUshortAsync((ushort)(value & 0xffff), cancellationToken); + await WriteLEUshortAsync((ushort)(value >> 16), cancellationToken); + } + /// /// Write a long in little endian byte order. /// @@ -535,6 +685,17 @@ public void WriteLELong(long value) WriteLEInt((int)(value >> 32)); } + /// + /// Write a long in little endian byte order. + /// + /// The value to write. + /// The that can be used to cancel the operation. + public async Task WriteLELongAsync(long value, CancellationToken cancellationToken) + { + await WriteLEIntAsync((int)value, cancellationToken); + await WriteLEIntAsync((int)(value >> 32), cancellationToken); + } + /// /// Write a ulong in little endian byte order. /// @@ -545,6 +706,17 @@ public void WriteLEUlong(ulong value) WriteLEUint((uint)(value >> 32)); } + /// + /// Write a ulong in little endian byte order. + /// + /// The value to write. + /// The that can be used to cancel the operation. + public async Task WriteLEUlongAsync(ulong value, CancellationToken cancellationToken) + { + await WriteLEUintAsync((uint)(value & 0xffffffff), cancellationToken); + await WriteLEUintAsync((uint)(value >> 32), cancellationToken); + } + #endregion LE value reading/writing /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index b9131d040..a0d9886bf 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Zip { @@ -166,6 +168,17 @@ private void WriteLeShort(int value) } } + /// + /// Write an unsigned short in little endian byte order. + /// + private async Task WriteLeShortAsync(int value, CancellationToken cancellationToken) + { + unchecked + { + await baseOutputStream_.WriteAsync(new[] {(byte)(value & 0xff), (byte)((value >> 8) & 0xff)}, 0, 2, cancellationToken); + } + } + /// /// Write an int in little endian byte order. /// @@ -178,6 +191,15 @@ private void WriteLeInt(int value) } } + /// + /// Write an int in little endian byte order. + /// + private async Task WriteLeIntAsync(int value, CancellationToken cancellationToken) + { + await WriteLeShortAsync(value, cancellationToken); + await WriteLeShortAsync(value >> 16, cancellationToken); + } + /// /// Write an int in little endian byte order. /// @@ -190,6 +212,18 @@ private void WriteLeLong(long value) } } + /// + /// Write an int in little endian byte order. + /// + private async Task WriteLeLongAsync(long value, CancellationToken cancellationToken) + { + unchecked + { + await WriteLeIntAsync((int)value, cancellationToken); + await WriteLeIntAsync((int)(value >> 32), cancellationToken); + } + } + // Apply any configured transforms/cleaning to the name of the supplied entry. private void TransformEntryName(ZipEntry entry) { @@ -457,56 +491,479 @@ public void PutNextEntry(ZipEntry entry) sizePatchPos += baseOutputStream_.Position; } - if (extra.Length > 0) + if (extra.Length > 0) + { + baseOutputStream_.Write(extra, 0, extra.Length); + } + + offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; + // Fix offsetOfCentraldir for AES + if (entry.AESKeySize > 0) + offset += entry.AESOverheadSize; + + // Activate the entry. + curEntry = entry; + crc.Reset(); + if (method == CompressionMethod.Deflated) + { + deflater_.Reset(); + deflater_.SetLevel(compressionLevel); + } + size = 0; + + if (entry.IsCrypted) + { + if (entry.AESKeySize > 0) + { + WriteAESHeader(entry); + } + else + { + if (entry.Crc < 0) + { // so testing Zip will says its ok + WriteEncryptionHeader(entry.DosTime << 16); + } + else + { + WriteEncryptionHeader(entry.Crc); + } + } + } + } + + /// + /// Starts a new Zip entry. It automatically closes the previous + /// entry if present. + /// All entry elements bar name are optional, but must be correct if present. + /// If the compression method is stored and the output is not patchable + /// the compression for that entry is automatically changed to deflate level 0 + /// + /// + /// the entry. + /// + /// The that can be used to cancel the operation. + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occured. + /// + /// + /// if stream was finished + /// + /// + /// 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. + /// + public async Task PutNextEntryAsync(ZipEntry entry, CancellationToken cancellationToken = default) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (entries == null) + { + throw new InvalidOperationException("ZipOutputStream was finished"); + } + + if (curEntry != null) + { + await CloseEntryAsync(cancellationToken); + } + + if (entries.Count == int.MaxValue) + { + throw new ZipException("Too many entries for Zip file"); + } + + CompressionMethod method = entry.CompressionMethod; + + // Check that the compression is one that we support + if (method != CompressionMethod.Deflated && method != CompressionMethod.Stored) + { + throw new NotImplementedException("Compression method not supported"); + } + + // A password must have been set in order to add AES encrypted entries + if (entry.AESKeySize > 0 && string.IsNullOrEmpty(this.Password)) + { + throw new InvalidOperationException("The Password property must be set before AES encrypted entries can be added"); + } + + int compressionLevel = defaultCompressionLevel; + + // Clear flags that the library manages internally + entry.Flags &= (int)GeneralBitFlags.UnicodeText; + patchEntryHeader = false; + + bool headerInfoAvailable; + + // No need to compress - definitely no data. + if (entry.Size == 0) + { + entry.CompressedSize = entry.Size; + entry.Crc = 0; + method = CompressionMethod.Stored; + headerInfoAvailable = true; + } + else + { + headerInfoAvailable = (entry.Size >= 0) && entry.HasCrc && entry.CompressedSize >= 0; + + // Switch to deflation if storing isnt possible. + if (method == CompressionMethod.Stored) + { + if (!headerInfoAvailable) + { + if (!CanPatchEntries) + { + // Can't patch entries so storing is not possible. + method = CompressionMethod.Deflated; + compressionLevel = 0; + } + } + else // entry.size must be > 0 + { + entry.CompressedSize = entry.Size; + headerInfoAvailable = entry.HasCrc; + } + } + } + + if (headerInfoAvailable == false) + { + if (CanPatchEntries == false) + { + // Only way to record size and compressed size is to append a data descriptor + // after compressed data. + + // Stored entries of this form have already been converted to deflating. + entry.Flags |= 8; + } + else + { + patchEntryHeader = true; + } + } + + if (Password != null) + { + entry.IsCrypted = true; + if (entry.Crc < 0) + { + // Need to append a data descriptor as the crc isnt available for use + // with encryption, the date is used instead. Setting the flag + // indicates this to the decompressor. + entry.Flags |= 8; + } + } + + entry.Offset = offset; + entry.CompressionMethod = (CompressionMethod)method; + + curMethod = method; + sizePatchPos = -1; + + if ((useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic))) + { + entry.ForceZip64(); + } + + // Write the local file header + await WriteLeIntAsync(ZipConstants.LocalHeaderSignature, cancellationToken); + + await WriteLeShortAsync(entry.Version, cancellationToken); + await WriteLeShortAsync(entry.Flags, cancellationToken); + await WriteLeShortAsync((byte)entry.CompressionMethodForHeader, cancellationToken); + await WriteLeIntAsync((int)entry.DosTime, cancellationToken); + + // TODO: Refactor header writing. Its done in several places. + if (headerInfoAvailable) + { + await WriteLeIntAsync((int)entry.Crc, cancellationToken); + if (entry.LocalHeaderRequiresZip64) + { + await WriteLeIntAsync(-1, cancellationToken); + await WriteLeIntAsync(-1, cancellationToken); + } + else + { + await WriteLeIntAsync((int)entry.CompressedSize + entry.EncryptionOverheadSize, cancellationToken); + await WriteLeIntAsync((int)entry.Size, cancellationToken); + } + } + else + { + if (patchEntryHeader) + { + crcPatchPos = baseOutputStream_.Position; + } + await WriteLeIntAsync(0, cancellationToken); // Crc + + if (patchEntryHeader) + { + sizePatchPos = baseOutputStream_.Position; + } + + // For local header both sizes appear in Zip64 Extended Information + if (entry.LocalHeaderRequiresZip64 || patchEntryHeader) + { + await WriteLeIntAsync(-1, cancellationToken); + await WriteLeIntAsync(-1, cancellationToken); + } + else + { + await WriteLeIntAsync(0, cancellationToken); // Compressed size + await WriteLeIntAsync(0, cancellationToken); // Uncompressed size + } + } + + // Apply any required transforms to the entry name, and then convert to byte array format. + TransformEntryName(entry); + byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xFFFF) + { + throw new ZipException("Entry name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64) + { + ed.StartNewEntry(); + if (headerInfoAvailable) + { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize + entry.EncryptionOverheadSize); + } + else + { + ed.AddLeLong(-1); + ed.AddLeLong(-1); + } + ed.AddNewEntry(1); + + if (!ed.Find(1)) + { + throw new ZipException("Internal error cant find extra data"); + } + + if (patchEntryHeader) + { + sizePatchPos = ed.CurrentReadIndex; + } + } + else + { + ed.Delete(1); + } + + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] extra = ed.GetEntryData(); + + await WriteLeShortAsync(name.Length, cancellationToken); + await WriteLeShortAsync(extra.Length, cancellationToken); + + if (name.Length > 0) + { + await baseOutputStream_.WriteAsync(name, 0, name.Length, cancellationToken); + } + + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) + { + sizePatchPos += baseOutputStream_.Position; + } + + if (extra.Length > 0) + { + await baseOutputStream_.WriteAsync(extra, 0, extra.Length, cancellationToken); + } + + offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; + // Fix offsetOfCentraldir for AES + if (entry.AESKeySize > 0) + offset += entry.AESOverheadSize; + + // Activate the entry. + curEntry = entry; + crc.Reset(); + if (method == CompressionMethod.Deflated) + { + deflater_.Reset(); + deflater_.SetLevel(compressionLevel); + } + size = 0; + + if (entry.IsCrypted) + { + if (entry.AESKeySize > 0) + { + await WriteAESHeaderAsync(entry, cancellationToken); + } + else + { + if (entry.Crc < 0) + { // so testing Zip will says its ok + await WriteEncryptionHeaderAsync(entry.DosTime << 16, cancellationToken); + } + else + { + await WriteEncryptionHeaderAsync(entry.Crc, cancellationToken); + } + } + } + } + + /// + /// Closes the current entry, updating header and footer information as required + /// + /// + /// An I/O error occurs. + /// + /// + /// No entry is active. + /// + public void CloseEntry() + { + if (curEntry == null) + { + throw new InvalidOperationException("No open entry"); + } + + long csize = size; + + // First finish the deflater, if appropriate + if (curMethod == CompressionMethod.Deflated) + { + if (size >= 0) + { + base.Finish(); + csize = deflater_.TotalOut; + } + else + { + deflater_.Reset(); + } + } + else if (curMethod == CompressionMethod.Stored) + { + // This is done by Finsh() for Deflated entries, but we need to do it + // ourselves for Stored ones + base.GetAuthCodeIfAES(); + } + + // Write the AES Authentication Code (a hash of the compressed and encrypted data) + if (curEntry.AESKeySize > 0) + { + baseOutputStream_.Write(AESAuthCode, 0, 10); + } + + if (curEntry.Size < 0) + { + curEntry.Size = size; + } + else if (curEntry.Size != size) + { + throw new ZipException("size was " + size + ", but I expected " + curEntry.Size); + } + + if (curEntry.CompressedSize < 0) + { + curEntry.CompressedSize = csize; + } + else if (curEntry.CompressedSize != csize) + { + throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize); + } + + if (curEntry.Crc < 0) + { + curEntry.Crc = crc.Value; + } + else if (curEntry.Crc != crc.Value) + { + throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc); + } + + offset += csize; + + if (curEntry.IsCrypted) + { + curEntry.CompressedSize += curEntry.EncryptionOverheadSize; + } + + // Patch the header if possible + if (patchEntryHeader) { - baseOutputStream_.Write(extra, 0, extra.Length); - } + patchEntryHeader = false; - offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; - // Fix offsetOfCentraldir for AES - if (entry.AESKeySize > 0) - offset += entry.AESOverheadSize; + long curPos = baseOutputStream_.Position; + baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); + WriteLeInt((int)curEntry.Crc); - // Activate the entry. - curEntry = entry; - crc.Reset(); - if (method == CompressionMethod.Deflated) - { - deflater_.Reset(); - deflater_.SetLevel(compressionLevel); + if (curEntry.LocalHeaderRequiresZip64) + { + if (sizePatchPos == -1) + { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + + baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); + WriteLeLong(curEntry.Size); + WriteLeLong(curEntry.CompressedSize); + } + else + { + WriteLeInt((int)curEntry.CompressedSize); + WriteLeInt((int)curEntry.Size); + } + baseOutputStream_.Seek(curPos, SeekOrigin.Begin); } - size = 0; - if (entry.IsCrypted) + // Add data descriptor if flagged as required + if ((curEntry.Flags & 8) != 0) { - if (entry.AESKeySize > 0) + WriteLeInt(ZipConstants.DataDescriptorSignature); + WriteLeInt(unchecked((int)curEntry.Crc)); + + if (curEntry.LocalHeaderRequiresZip64) { - WriteAESHeader(entry); + WriteLeLong(curEntry.CompressedSize); + WriteLeLong(curEntry.Size); + offset += ZipConstants.Zip64DataDescriptorSize; } else { - if (entry.Crc < 0) - { // so testing Zip will says its ok - WriteEncryptionHeader(entry.DosTime << 16); - } - else - { - WriteEncryptionHeader(entry.Crc); - } + WriteLeInt((int)curEntry.CompressedSize); + WriteLeInt((int)curEntry.Size); + offset += ZipConstants.DataDescriptorSize; } } + + entries.Add(curEntry); + curEntry = null; } /// /// Closes the current entry, updating header and footer information as required /// + /// The that can be used to cancel the operation. /// /// An I/O error occurs. /// /// /// No entry is active. /// - public void CloseEntry() + public async Task CloseEntryAsync(CancellationToken cancellationToken) { if (curEntry == null) { @@ -520,7 +977,7 @@ public void CloseEntry() { if (size >= 0) { - base.Finish(); + await base.FinishAsync(cancellationToken); csize = deflater_.TotalOut; } else @@ -538,7 +995,7 @@ public void CloseEntry() // Write the AES Authentication Code (a hash of the compressed and encrypted data) if (curEntry.AESKeySize > 0) { - baseOutputStream_.Write(AESAuthCode, 0, 10); + await baseOutputStream_.WriteAsync(AESAuthCode, 0, 10, cancellationToken); } if (curEntry.Size < 0) @@ -582,7 +1039,7 @@ public void CloseEntry() long curPos = baseOutputStream_.Position; baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); - WriteLeInt((int)curEntry.Crc); + await WriteLeIntAsync((int)curEntry.Crc, cancellationToken); if (curEntry.LocalHeaderRequiresZip64) { @@ -592,13 +1049,13 @@ public void CloseEntry() } baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); - WriteLeLong(curEntry.Size); - WriteLeLong(curEntry.CompressedSize); + await WriteLeLongAsync(curEntry.Size, cancellationToken); + await WriteLeLongAsync(curEntry.CompressedSize, cancellationToken); } else { - WriteLeInt((int)curEntry.CompressedSize); - WriteLeInt((int)curEntry.Size); + await WriteLeIntAsync((int)curEntry.CompressedSize, cancellationToken); + await WriteLeIntAsync((int)curEntry.Size, cancellationToken); } baseOutputStream_.Seek(curPos, SeekOrigin.Begin); } @@ -606,19 +1063,19 @@ public void CloseEntry() // Add data descriptor if flagged as required if ((curEntry.Flags & 8) != 0) { - WriteLeInt(ZipConstants.DataDescriptorSignature); - WriteLeInt(unchecked((int)curEntry.Crc)); + await WriteLeIntAsync(ZipConstants.DataDescriptorSignature, cancellationToken); + await WriteLeIntAsync(unchecked((int)curEntry.Crc), cancellationToken); if (curEntry.LocalHeaderRequiresZip64) { - WriteLeLong(curEntry.CompressedSize); - WriteLeLong(curEntry.Size); + await WriteLeLongAsync(curEntry.CompressedSize, cancellationToken); + await WriteLeLongAsync(curEntry.Size, cancellationToken); offset += ZipConstants.Zip64DataDescriptorSize; } else { - WriteLeInt((int)curEntry.CompressedSize); - WriteLeInt((int)curEntry.Size); + await WriteLeIntAsync((int)curEntry.CompressedSize, cancellationToken); + await WriteLeIntAsync((int)curEntry.Size, cancellationToken); offset += ZipConstants.DataDescriptorSize; } } @@ -645,6 +1102,24 @@ private void WriteEncryptionHeader(long crcValue) baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length); } + private async Task WriteEncryptionHeaderAsync(long crcValue, CancellationToken cancellationToken) + { + offset += ZipConstants.CryptoHeaderSize; + + InitializePassword(Password); + + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + using (var rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(cryptBuffer); + } + + cryptBuffer[11] = (byte)(crcValue >> 24); + + EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); + await baseOutputStream_.WriteAsync(cryptBuffer, 0, cryptBuffer.Length, cancellationToken); + } + private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) { // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. @@ -683,6 +1158,28 @@ private void WriteAESHeader(ZipEntry entry) baseOutputStream_.Write(pwdVerifier, 0, pwdVerifier.Length); } + // Replaces WriteEncryptionHeader for AES + // + private async Task WriteAESHeaderAsync(ZipEntry entry, CancellationToken cancellationToken) + { + byte[] salt; + byte[] pwdVerifier; + InitializeAESPassword(entry, Password, out salt, out pwdVerifier); + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. + await baseOutputStream_.WriteAsync(salt, 0, salt.Length, cancellationToken); + await baseOutputStream_.WriteAsync(pwdVerifier, 0, pwdVerifier.Length, cancellationToken); + } + /// /// Writes the given buffer to the current entry. /// @@ -925,6 +1422,179 @@ public override void Finish() entries = null; } + /// + /// Finishes the stream. This will write the central directory at the + /// end of the zip file and flush the stream. + /// + /// The that can be used to cancel the operation. + /// + /// This is automatically called when the stream is closed. + /// + /// + /// An I/O error occurs. + /// + /// + /// Comment exceeds the maximum length
+ /// Entry name exceeds the maximum length + ///
+ public override async Task FinishAsync(CancellationToken cancellationToken) + { + if (entries == null) + { + return; + } + + if (curEntry != null) + { + await CloseEntryAsync(cancellationToken); + } + + long numEntries = entries.Count; + long sizeEntries = 0; + + foreach (ZipEntry entry in entries) + { + await WriteLeIntAsync(ZipConstants.CentralHeaderSignature, cancellationToken); + await WriteLeShortAsync((entry.HostSystem << 8) | entry.VersionMadeBy, cancellationToken); + await WriteLeShortAsync(entry.Version, cancellationToken); + await WriteLeShortAsync(entry.Flags, cancellationToken); + await WriteLeShortAsync((short)entry.CompressionMethodForHeader, cancellationToken); + await WriteLeIntAsync((int)entry.DosTime, cancellationToken); + await WriteLeIntAsync((int)entry.Crc, cancellationToken); + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= uint.MaxValue)) + { + await WriteLeIntAsync(-1, cancellationToken); + } + else + { + await WriteLeIntAsync((int)entry.CompressedSize, cancellationToken); + } + + if (entry.IsZip64Forced() || + (entry.Size >= uint.MaxValue)) + { + await WriteLeIntAsync(-1, cancellationToken); + } + else + { + await WriteLeIntAsync((int)entry.Size, cancellationToken); + } + + byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xffff) + { + throw new ZipException("Name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.CentralHeaderRequiresZip64) + { + ed.StartNewEntry(); + if (entry.IsZip64Forced() || + (entry.Size >= 0xffffffff)) + { + ed.AddLeLong(entry.Size); + } + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= 0xffffffff)) + { + ed.AddLeLong(entry.CompressedSize); + } + + if (entry.Offset >= 0xffffffff) + { + ed.AddLeLong(entry.Offset); + } + + ed.AddNewEntry(1); + } + else + { + ed.Delete(1); + } + + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] extra = ed.GetEntryData(); + + byte[] entryComment = + (entry.Comment != null) ? + ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : + new byte[0]; + + if (entryComment.Length > 0xffff) + { + throw new ZipException("Comment too long."); + } + + await WriteLeShortAsync(name.Length, cancellationToken); + await WriteLeShortAsync(extra.Length, cancellationToken); + await WriteLeShortAsync(entryComment.Length, cancellationToken); + await WriteLeShortAsync(0, cancellationToken); // disk number + await WriteLeShortAsync(0, cancellationToken); // internal file attributes + // external file attributes + + if (entry.ExternalFileAttributes != -1) + { + await WriteLeIntAsync(entry.ExternalFileAttributes, cancellationToken); + } + else + { + if (entry.IsDirectory) + { // mark entry as directory (from nikolam.AT.perfectinfo.com) + await WriteLeIntAsync(16, cancellationToken); + } + else + { + await WriteLeIntAsync(0, cancellationToken); + } + } + + if (entry.Offset >= uint.MaxValue) + { + await WriteLeIntAsync(-1, cancellationToken); + } + else + { + await WriteLeIntAsync((int)entry.Offset, cancellationToken); + } + + if (name.Length > 0) + { + await baseOutputStream_.WriteAsync(name, 0, name.Length, cancellationToken); + } + + if (extra.Length > 0) + { + await baseOutputStream_.WriteAsync(extra, 0, extra.Length, cancellationToken); + } + + if (entryComment.Length > 0) + { + await baseOutputStream_.WriteAsync(entryComment, 0, entryComment.Length, cancellationToken); + } + + sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + } + +#if NETSTANDARD2_1 + await +#endif + using (ZipHelperStream zhs = new ZipHelperStream(baseOutputStream_)) + { + await zhs.WriteEndOfCentralDirectoryAsync(numEntries, sizeEntries, offset, zipComment, cancellationToken); + } + + entries = null; + } + /// /// Flushes the stream by calling Flush on the deflater stream unless /// the current compression method is . Then it flushes the underlying output stream. From b83bbb7249266b109779697893b221ffb3a297d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 14 Feb 2021 00:51:00 +0100 Subject: [PATCH 2/7] Split out (a)sync method logic and unify LE read/writes --- .../Core/ByteOrderUtils.cs | 127 ++ .../Core/StreamUtils.cs | 22 +- .../Zip/ZipExtraData.cs | 53 +- src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs | 49 +- src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs | 399 ++++++ .../Zip/ZipHelperStream.cs | 801 ------------ .../Zip/ZipOutputStream.cs | 1136 ++++------------- 7 files changed, 859 insertions(+), 1728 deletions(-) create mode 100644 src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs create mode 100644 src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs delete mode 100644 src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs diff --git a/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs b/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs new file mode 100644 index 000000000..0edb0964c --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs @@ -0,0 +1,127 @@ +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using CT = System.Threading.CancellationToken; + +namespace ICSharpCode.SharpZipLib.Core +{ + internal static class ByteOrderStreamExtensions + { + static byte[] SwappedBytes(ushort value) => new[] {(byte)value, (byte)(value >> 8)}; + static byte[] SwappedBytes(short value) => new[] {(byte)value, (byte)(value >> 8)}; + static byte[] SwappedBytes(uint value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; + static byte[] SwappedBytes(int value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; + + static byte[] SwappedBytes(long value) => new[] { + (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), + (byte)(value << 32), (byte)(value << 40), (byte)(value << 48), (byte)(value << 56) + }; + + static byte[] SwappedBytes(ulong value) => new[] { + (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), + (byte)(value << 32), (byte)(value << 40), (byte)(value << 48), (byte)(value << 56) + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static long SwappedS64(byte[] bytes) => ( + (long)bytes[0] << 0 | (long)bytes[1] << 8 | (long)bytes[2] << 16 | (long)bytes[3] << 24 | + (long)bytes[4] << 32 | (long)bytes[5] << 40 | (long)bytes[6] << 48 | (long)bytes[7] << 56); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ulong SwappedU64(byte[] bytes) => ( + (ulong)bytes[0] << 0 | (ulong)bytes[1] << 8 | (ulong)bytes[2] << 16 | (ulong)bytes[3] << 24 | + (ulong)bytes[4] << 32 | (ulong)bytes[5] << 40 | (ulong)bytes[6] << 48 | (ulong)bytes[7] << 56); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int SwappedS32(byte[] bytes) => bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static uint SwappedU32(byte[] bytes) => (uint) SwappedS32(bytes); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static short SwappedS16(byte[] bytes) => (short)(bytes[0] | bytes[1] << 8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ushort SwappedU16(byte[] bytes) => (ushort) SwappedS16(bytes); + + internal static byte[] ReadBytes(this Stream stream, int count) + { + var bytes = new byte[count]; + var remaining = count; + while (remaining > 0) + { + var bytesRead = stream.Read(bytes, count - remaining, remaining); + if (bytesRead < 1) throw new EndOfStreamException(); + remaining -= bytesRead; + } + + return bytes; + } + + /// Read an unsigned short in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadLEShort(this Stream stream) => SwappedS16(ReadBytes(stream, 2)); + + /// Read an int in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadLEInt(this Stream stream) => SwappedS32(ReadBytes(stream, 4)); + + /// Read a long in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ReadLELong(this Stream stream) => SwappedS64(ReadBytes(stream, 8)); + + /// Write an unsigned short in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEShort(this Stream stream, int value) => stream.Write(SwappedBytes(value), 0, 2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEShortAsync(this Stream stream, int value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 2, ct); + + /// Write a ushort in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEUshort(this Stream stream, ushort value) => stream.Write(SwappedBytes(value), 0, 2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEUshortAsync(this Stream stream, ushort value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 2, ct); + + /// Write an int in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEInt(this Stream stream, int value) => stream.Write(SwappedBytes(value), 0, 4); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEIntAsync(this Stream stream, int value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 4, ct); + + /// Write a uint in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEUint(this Stream stream, uint value) => stream.Write(SwappedBytes(value), 0, 4); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEUintAsync(this Stream stream, uint value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 4, ct); + + /// Write a long in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLELong(this Stream stream, long value) => stream.Write(SwappedBytes(value), 0, 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLELongAsync(this Stream stream, long value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 8, ct); + + /// Write a ulong in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEUlong(this Stream stream, ulong value) => stream.Write(SwappedBytes(value), 0, 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEUlongAsync(this Stream stream, ulong value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 8, ct); + } +} diff --git a/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs b/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs index 6d0d9b304..a46ee9337 100644 --- a/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs +++ b/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs @@ -6,7 +6,7 @@ namespace ICSharpCode.SharpZipLib.Core /// /// Provides simple " utilities. /// - public sealed class StreamUtils + public static class StreamUtils { /// /// Read from a ensuring all the required data is read. @@ -14,7 +14,7 @@ public sealed class StreamUtils /// The stream to read. /// The buffer to fill. /// - static public void ReadFully(Stream stream, byte[] buffer) + public static void ReadFully(Stream stream, byte[] buffer) { ReadFully(stream, buffer, 0, buffer.Length); } @@ -29,7 +29,7 @@ static public void ReadFully(Stream stream, byte[] buffer) /// Required parameter is null /// and or are invalid. /// End of stream is encountered before all the data has been read. - static public void ReadFully(Stream stream, byte[] buffer, int offset, int count) + public static void ReadFully(Stream stream, byte[] buffer, int offset, int count) { if (stream == null) { @@ -73,7 +73,7 @@ static public void ReadFully(Stream stream, byte[] buffer, int offset, int count /// The number of bytes of data to store. /// Required parameter is null /// and or are invalid. - static public int ReadRequestedBytes(Stream stream, byte[] buffer, int offset, int count) + public static int ReadRequestedBytes(Stream stream, byte[] buffer, int offset, int count) { if (stream == null) { @@ -118,7 +118,7 @@ static public int ReadRequestedBytes(Stream stream, byte[] buffer, int offset, i /// The stream to source data from. /// The stream to write data to. /// The buffer to use during copying. - static public void Copy(Stream source, Stream destination, byte[] buffer) + public static void Copy(Stream source, Stream destination, byte[] buffer) { if (source == null) { @@ -169,7 +169,7 @@ static public void Copy(Stream source, Stream destination, byte[] buffer) /// The source for this event. /// The name to use with the event. /// This form is specialised for use within #Zip to support events during archive operations. - static public void Copy(Stream source, Stream destination, + public static void Copy(Stream source, Stream destination, byte[] buffer, ProgressHandler progressHandler, TimeSpan updateInterval, object sender, string name) { Copy(source, destination, buffer, progressHandler, updateInterval, sender, name, -1); @@ -188,7 +188,7 @@ static public void Copy(Stream source, Stream destination, /// A predetermined fixed target value to use with progress updates. /// If the value is negative the target is calculated by looking at the stream. /// This form is specialised for use within #Zip to support events during archive operations. - static public void Copy(Stream source, Stream destination, + public static void Copy(Stream source, Stream destination, byte[] buffer, ProgressHandler progressHandler, TimeSpan updateInterval, object sender, string name, long fixedTarget) @@ -272,13 +272,5 @@ static public void Copy(Stream source, Stream destination, progressHandler(sender, args); } } - - /// - /// Initialise an instance of - /// - private StreamUtils() - { - // Do nothing. - } } } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs index 0535b1250..e08c1bf65 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using ICSharpCode.SharpZipLib.Core; namespace ICSharpCode.SharpZipLib.Zip { @@ -152,16 +153,15 @@ public short TagID public void SetData(byte[] data, int index, int count) { using (MemoryStream ms = new MemoryStream(data, index, count, false)) - using (ZipHelperStream helperStream = new ZipHelperStream(ms)) { // bit 0 if set, modification time is present // bit 1 if set, access time is present // bit 2 if set, creation time is present - _flags = (Flags)helperStream.ReadByte(); + _flags = (Flags)ms.ReadByte(); if (((_flags & Flags.ModificationTime) != 0)) { - int iTime = helperStream.ReadLEInt(); + int iTime = ms.ReadLEInt(); _modificationTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + new TimeSpan(0, 0, 0, iTime, 0); @@ -172,7 +172,7 @@ public void SetData(byte[] data, int index, int count) if ((_flags & Flags.AccessTime) != 0) { - int iTime = helperStream.ReadLEInt(); + int iTime = ms.ReadLEInt(); _lastAccessTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + new TimeSpan(0, 0, 0, iTime, 0); @@ -180,7 +180,7 @@ public void SetData(byte[] data, int index, int count) if ((_flags & Flags.CreateTime) != 0) { - int iTime = helperStream.ReadLEInt(); + int iTime = ms.ReadLEInt(); _createTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + new TimeSpan(0, 0, 0, iTime, 0); @@ -195,27 +195,25 @@ public void SetData(byte[] data, int index, int count) public byte[] GetData() { using (MemoryStream ms = new MemoryStream()) - using (ZipHelperStream helperStream = new ZipHelperStream(ms)) { - helperStream.IsStreamOwner = false; - helperStream.WriteByte((byte)_flags); // Flags + ms.WriteByte((byte)_flags); // Flags if ((_flags & Flags.ModificationTime) != 0) { TimeSpan span = _modificationTime - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); var seconds = (int)span.TotalSeconds; - helperStream.WriteLEInt(seconds); + ms.WriteLEInt(seconds); } if ((_flags & Flags.AccessTime) != 0) { TimeSpan span = _lastAccessTime - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); var seconds = (int)span.TotalSeconds; - helperStream.WriteLEInt(seconds); + ms.WriteLEInt(seconds); } if ((_flags & Flags.CreateTime) != 0) { TimeSpan span = _createTime - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); var seconds = (int)span.TotalSeconds; - helperStream.WriteLEInt(seconds); + ms.WriteLEInt(seconds); } return ms.ToArray(); } @@ -341,24 +339,23 @@ public short TagID public void SetData(byte[] data, int index, int count) { using (MemoryStream ms = new MemoryStream(data, index, count, false)) - using (ZipHelperStream helperStream = new ZipHelperStream(ms)) { - helperStream.ReadLEInt(); // Reserved - while (helperStream.Position < helperStream.Length) + ms.ReadLEInt(); // Reserved + while (ms.Position < ms.Length) { - int ntfsTag = helperStream.ReadLEShort(); - int ntfsLength = helperStream.ReadLEShort(); + int ntfsTag = ms.ReadLEShort(); + int ntfsLength = ms.ReadLEShort(); if (ntfsTag == 1) { if (ntfsLength >= 24) { - long lastModificationTicks = helperStream.ReadLELong(); + long lastModificationTicks = ms.ReadLELong(); _lastModificationTime = DateTime.FromFileTimeUtc(lastModificationTicks); - long lastAccessTicks = helperStream.ReadLELong(); + long lastAccessTicks = ms.ReadLELong(); _lastAccessTime = DateTime.FromFileTimeUtc(lastAccessTicks); - long createTimeTicks = helperStream.ReadLELong(); + long createTimeTicks = ms.ReadLELong(); _createTime = DateTime.FromFileTimeUtc(createTimeTicks); } break; @@ -366,7 +363,7 @@ public void SetData(byte[] data, int index, int count) else { // An unknown NTFS tag so simply skip it. - helperStream.Seek(ntfsLength, SeekOrigin.Current); + ms.Seek(ntfsLength, SeekOrigin.Current); } } } @@ -379,15 +376,13 @@ public void SetData(byte[] data, int index, int count) public byte[] GetData() { using (MemoryStream ms = new MemoryStream()) - using (ZipHelperStream helperStream = new ZipHelperStream(ms)) - { - helperStream.IsStreamOwner = false; - helperStream.WriteLEInt(0); // Reserved - helperStream.WriteLEShort(1); // Tag - helperStream.WriteLEShort(24); // Length = 3 x 8. - helperStream.WriteLELong(_lastModificationTime.ToFileTimeUtc()); - helperStream.WriteLELong(_lastAccessTime.ToFileTimeUtc()); - helperStream.WriteLELong(_createTime.ToFileTimeUtc()); + { + ms.WriteLEInt(0); // Reserved + ms.WriteLEShort(1); // Tag + ms.WriteLEShort(24); // Length = 3 x 8. + ms.WriteLELong(_lastModificationTime.ToFileTimeUtc()); + ms.WriteLELong(_lastAccessTime.ToFileTimeUtc()); + ms.WriteLELong(_createTime.ToFileTimeUtc()); return ms.ToArray(); } } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs index 62015a932..80baca361 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs @@ -997,9 +997,8 @@ public bool TestArchive(bool testData, TestStrategy strategy, ZipTestResultHandl if ((this[entryIndex].Flags & (int)GeneralBitFlags.Descriptor) != 0) { - var helper = new ZipHelperStream(baseStream_); var data = new DescriptorData(); - helper.ReadDataDescriptor(this[entryIndex].LocalHeaderRequiresZip64, data); + ZipFormat.ReadDataDescriptor(baseStream_, this[entryIndex].LocalHeaderRequiresZip64, data); if (this[entryIndex].Crc != data.Crc) { status.AddError(); @@ -1575,10 +1574,7 @@ public void CommitUpdate() if (entries_.Length == 0) { byte[] theComment = (newComment_ != null) ? newComment_.RawComment : ZipStrings.ConvertToArray(comment_); - using (ZipHelperStream zhs = new ZipHelperStream(baseStream_)) - { - zhs.WriteEndOfCentralDirectory(0, 0, 0, theComment); - } + ZipFormat.WriteEndOfCentralDirectory(baseStream_, 0, 0, 0, theComment); } } } @@ -2719,8 +2715,7 @@ private void AddEntry(ZipFile workFile, ZipUpdate update) if ((update.OutEntry.Flags & (int)GeneralBitFlags.Descriptor) == (int)GeneralBitFlags.Descriptor) { - var helper = new ZipHelperStream(workFile.baseStream_); - helper.WriteDataDescriptor(update.OutEntry); + ZipFormat.WriteDataDescriptor(workFile.baseStream_, update.OutEntry); } } } @@ -2857,15 +2852,11 @@ private void UpdateCommentOnly() { long baseLength = baseStream_.Length; - ZipHelperStream updateFile = null; + Stream updateFile; if (archiveStorage_.UpdateMode == FileUpdateMode.Safe) { - Stream copyStream = archiveStorage_.MakeTemporaryCopy(baseStream_); - updateFile = new ZipHelperStream(copyStream) - { - IsStreamOwner = true - }; + updateFile = archiveStorage_.MakeTemporaryCopy(baseStream_); baseStream_.Dispose(); baseStream_ = null; @@ -2882,21 +2873,21 @@ private void UpdateCommentOnly() // Need to tidy up the archive storage interface and contract basically. baseStream_ = archiveStorage_.OpenForDirectUpdate(baseStream_); - updateFile = new ZipHelperStream(baseStream_); + updateFile = baseStream_; } else { baseStream_.Dispose(); baseStream_ = null; - updateFile = new ZipHelperStream(Name); + updateFile = new FileStream(Name, FileMode.Open, FileAccess.ReadWrite); } } - using (updateFile) + try { long locatedCentralDirOffset = - updateFile.LocateBlockWithSignature(ZipConstants.EndOfCentralDirectorySignature, - baseLength, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + ZipFormat.LocateBlockWithSignature(updateFile, ZipConstants.EndOfCentralDirectorySignature, + baseLength, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); if (locatedCentralDirOffset < 0) { throw new ZipException("Cannot find central directory"); @@ -2911,6 +2902,11 @@ private void UpdateCommentOnly() updateFile.Write(rawComment, 0, rawComment.Length); updateFile.SetLength(updateFile.Position); } + finally + { + if(updateFile != baseStream_) + updateFile.Dispose(); + } if (archiveStorage_.UpdateMode == FileUpdateMode.Safe) { @@ -3073,10 +3069,8 @@ private void RunUpdates() } byte[] theComment = (newComment_ != null) ? newComment_.RawComment : ZipStrings.ConvertToArray(comment_); - using (ZipHelperStream zhs = new ZipHelperStream(workFile.baseStream_)) - { - zhs.WriteEndOfCentralDirectory(updateCount_, sizeEntries, centralDirOffset, theComment); - } + ZipFormat.WriteEndOfCentralDirectory(workFile.baseStream_, updateCount_, + sizeEntries, centralDirOffset, theComment); endOfStream = workFile.baseStream_.Position; @@ -3417,13 +3411,8 @@ private ulong ReadLEUlong() #endregion Reading // NOTE this returns the offset of the first byte after the signature. - private long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) - { - using (ZipHelperStream les = new ZipHelperStream(baseStream_)) - { - return les.LocateBlockWithSignature(signature, endLocation, minimumBlockSize, maximumVariableData); - } - } + private long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + => ZipFormat.LocateBlockWithSignature(baseStream_, signature, endLocation, minimumBlockSize, maximumVariableData); /// /// Search for and read the central directory of a zip file filling the entries array. diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs new file mode 100644 index 000000000..224a87eeb --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs @@ -0,0 +1,399 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ICSharpCode.SharpZipLib.Core; + +namespace ICSharpCode.SharpZipLib.Zip +{ + /// + /// Holds data pertinent to a data descriptor. + /// + public class DescriptorData + { + private long _crc; + + /// + /// Get /set the compressed size of data. + /// + public long CompressedSize { get; set; } + + /// + /// Get / set the uncompressed size of data + /// + public long Size { get; set; } + + /// + /// Get /set the crc value. + /// + public long Crc + { + get => _crc; + set => _crc = (value & 0xffffffff); + } + } + + internal class EntryPatchData + { + public long SizePatchOffset { get; set; } + + public long CrcPatchOffset { get; set; } + } + + /// + /// This class assists with writing/reading from Zip files. + /// + internal static class ZipFormat + { + // Write the local file header + // TODO: ZipFormat.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage + private static void WriteLocalHeader(Stream stream, ZipEntry entry, EntryPatchData patchData, + bool headerInfoAvailable, bool patchEntryHeader, int offset) + { + CompressionMethod method = entry.CompressionMethod; + + stream.WriteLEInt(ZipConstants.LocalHeaderSignature); + + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((byte)method); + stream.WriteLEInt((int)entry.DosTime); + + if (headerInfoAvailable) + { + stream.WriteLEInt((int)entry.Crc); + if (entry.LocalHeaderRequiresZip64) + { + stream.WriteLEInt(-1); + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); + stream.WriteLEInt((int)entry.Size); + } + } + else + { + if (patchData != null) + { + patchData.CrcPatchOffset = offset + stream.Position; + } + stream.WriteLEInt(0); // Crc + + if (patchData != null) + { + patchData.SizePatchOffset = offset + stream.Position; + } + + // For local header both sizes appear in Zip64 Extended Information + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) + { + stream.WriteLEInt(-1); + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt(0); // Compressed size + stream.WriteLEInt(0); // Uncompressed size + } + } + + byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xFFFF) + { + throw new ZipException("Entry name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader)) + { + ed.StartNewEntry(); + if (headerInfoAvailable) + { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + } + else + { + ed.AddLeLong(-1); + ed.AddLeLong(-1); + } + ed.AddNewEntry(1); + + if (!ed.Find(1)) + { + throw new ZipException("Internal error cant find extra data"); + } + + if (patchData != null) + { + patchData.SizePatchOffset = ed.CurrentReadIndex; + } + } + else + { + ed.Delete(1); + } + + byte[] extra = ed.GetEntryData(); + + stream.WriteLEShort(name.Length); + stream.WriteLEShort(extra.Length); + + if (name.Length > 0) + { + stream.Write(name, 0, name.Length); + } + + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader && patchData != null) + { + patchData.SizePatchOffset += offset + stream.Position; + } + + if (extra.Length > 0) + { + stream.Write(extra, 0, extra.Length); + } + } + + /// + /// Locates a block with the desired . + /// + /// + /// The signature to find. + /// Location, marking the end of block. + /// Minimum size of the block. + /// The maximum variable data. + /// Returns the offset of the first byte after the signature; -1 if not found + internal static long LocateBlockWithSignature(Stream stream, int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + { + long pos = endLocation - minimumBlockSize; + if (pos < 0) + { + return -1; + } + + long giveUpMarker = Math.Max(pos - maximumVariableData, 0); + + // TODO: This loop could be optimized for speed. + do + { + if (pos < giveUpMarker) + { + return -1; + } + stream.Seek(pos--, SeekOrigin.Begin); + } while (stream.ReadLEInt() != signature); + + return stream.Position; + } + + /// + public static async Task WriteZip64EndOfCentralDirectoryAsync(Stream stream, long noOfEntries, + long sizeEntries, long centralDirOffset, CancellationToken cancellationToken) + { + using (var ms = new MemoryStream()) + { + WriteZip64EndOfCentralDirectory(ms, noOfEntries, sizeEntries, centralDirOffset); + await ms.CopyToAsync(stream, 81920, cancellationToken); + } + } + + /// + /// Write Zip64 end of central directory records (File header and locator). + /// + /// + /// The number of entries in the central directory. + /// The size of entries in the central directory. + /// The offset of the central directory. + internal static void WriteZip64EndOfCentralDirectory(Stream stream, long noOfEntries, long sizeEntries, long centralDirOffset) + { + long centralSignatureOffset = centralDirOffset + sizeEntries; + stream.WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature); + stream.WriteLELong(44); // Size of this record (total size of remaining fields in header or full size - 12) + stream.WriteLEShort(ZipConstants.VersionMadeBy); // Version made by + stream.WriteLEShort(ZipConstants.VersionZip64); // Version to extract + stream.WriteLEInt(0); // Number of this disk + stream.WriteLEInt(0); // number of the disk with the start of the central directory + stream.WriteLELong(noOfEntries); // No of entries on this disk + stream.WriteLELong(noOfEntries); // Total No of entries in central directory + stream.WriteLELong(sizeEntries); // Size of the central directory + stream.WriteLELong(centralDirOffset); // offset of start of central directory + // zip64 extensible data sector not catered for here (variable size) + + // Write the Zip64 end of central directory locator + stream.WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature); + + // no of the disk with the start of the zip64 end of central directory + stream.WriteLEInt(0); + + // relative offset of the zip64 end of central directory record + stream.WriteLELong(centralSignatureOffset); + + // total number of disks + stream.WriteLEInt(1); + } + + /// + public static async Task WriteEndOfCentralDirectoryAsync(Stream stream, long noOfEntries, long sizeEntries, + long start, byte[] comment, CancellationToken cancellationToken) + { + using (var ms = new MemoryStream()) + { + WriteEndOfCentralDirectory(ms, noOfEntries, sizeEntries, start, comment); + await ms.CopyToAsync(stream, 81920, cancellationToken); + } + } + + + /// + /// Write the required records to end the central directory. + /// + /// + /// The number of entries in the directory. + /// The size of the entries in the directory. + /// The start of the central directory. + /// The archive comment. (This can be null). + + internal static void WriteEndOfCentralDirectory(Stream stream, long noOfEntries, long sizeEntries, long start, byte[] comment) + { + if (noOfEntries >= 0xffff || + start >= 0xffffffff || + sizeEntries >= 0xffffffff) + { + WriteZip64EndOfCentralDirectory(stream, noOfEntries, sizeEntries, start); + } + + stream.WriteLEInt(ZipConstants.EndOfCentralDirectorySignature); + + // TODO: ZipFile Multi disk handling not done + stream.WriteLEShort(0); // number of this disk + stream.WriteLEShort(0); // no of disk with start of central dir + + // Number of entries + if (noOfEntries >= 0xffff) + { + stream.WriteLEUshort(0xffff); // Zip64 marker + stream.WriteLEUshort(0xffff); + } + else + { + stream.WriteLEShort((short)noOfEntries); // entries in central dir for this disk + stream.WriteLEShort((short)noOfEntries); // total entries in central directory + } + + // Size of the central directory + if (sizeEntries >= 0xffffffff) + { + stream.WriteLEUint(0xffffffff); // Zip64 marker + } + else + { + stream.WriteLEInt((int)sizeEntries); + } + + // offset of start of central directory + if (start >= 0xffffffff) + { + stream.WriteLEUint(0xffffffff); // Zip64 marker + } + else + { + stream.WriteLEInt((int)start); + } + + var commentLength = comment?.Length ?? 0; + + if (commentLength > 0xffff) + { + throw new ZipException($"Comment length ({commentLength}) is larger than 64K"); + } + + stream.WriteLEShort(commentLength); + + if (commentLength > 0) + { + stream.Write(comment, 0, commentLength); + } + } + + + + /// + /// Write a data descriptor. + /// + /// + /// The entry to write a descriptor for. + /// Returns the number of descriptor bytes written. + internal static int WriteDataDescriptor(Stream stream, ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + int result = 0; + + // Add data descriptor if flagged as required + if ((entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) + { + // The signature is not PKZIP originally but is now described as optional + // in the PKZIP Appnote documenting the format. + stream.WriteLEInt(ZipConstants.DataDescriptorSignature); + stream.WriteLEInt(unchecked((int)(entry.Crc))); + + result += 8; + + if (entry.LocalHeaderRequiresZip64) + { + stream.WriteLELong(entry.CompressedSize); + stream.WriteLELong(entry.Size); + result += 16; + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + stream.WriteLEInt((int)entry.Size); + result += 8; + } + } + + return result; + } + + /// + /// Read data descriptor at the end of compressed data. + /// + /// + /// if set to true [zip64]. + /// The data to fill in. + /// Returns the number of bytes read in the descriptor. + internal static void ReadDataDescriptor(Stream stream, bool zip64, DescriptorData data) + { + int intValue = stream.ReadLEInt(); + + // In theory this may not be a descriptor according to PKZIP appnote. + // In practice its always there. + if (intValue != ZipConstants.DataDescriptorSignature) + { + throw new ZipException("Data descriptor signature not found"); + } + + data.Crc = stream.ReadLEInt(); + + if (zip64) + { + data.CompressedSize = stream.ReadLELong(); + data.Size = stream.ReadLELong(); + } + else + { + data.CompressedSize = stream.ReadLEInt(); + data.Size = stream.ReadLEInt(); + } + } + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs deleted file mode 100644 index f9c277ff2..000000000 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs +++ /dev/null @@ -1,801 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace ICSharpCode.SharpZipLib.Zip -{ - /// - /// Holds data pertinent to a data descriptor. - /// - public class DescriptorData - { - /// - /// Get /set the compressed size of data. - /// - public long CompressedSize - { - get { return compressedSize; } - set { compressedSize = value; } - } - - /// - /// Get / set the uncompressed size of data - /// - public long Size - { - get { return size; } - set { size = value; } - } - - /// - /// Get /set the crc value. - /// - public long Crc - { - get { return crc; } - set { crc = (value & 0xffffffff); } - } - - #region Instance Fields - - private long size; - private long compressedSize; - private long crc; - - #endregion Instance Fields - } - - internal class EntryPatchData - { - public long SizePatchOffset - { - get { return sizePatchOffset_; } - set { sizePatchOffset_ = value; } - } - - public long CrcPatchOffset - { - get { return crcPatchOffset_; } - set { crcPatchOffset_ = value; } - } - - #region Instance Fields - - private long sizePatchOffset_; - private long crcPatchOffset_; - - #endregion Instance Fields - } - - /// - /// This class assists with writing/reading from Zip files. - /// - internal class ZipHelperStream : Stream - { - #region Constructors - - /// - /// Initialise an instance of this class. - /// - /// The name of the file to open. - public ZipHelperStream(string name) - { - stream_ = new FileStream(name, FileMode.Open, FileAccess.ReadWrite); - isOwner_ = true; - } - - /// - /// Initialise a new instance of . - /// - /// The stream to use. - public ZipHelperStream(Stream stream) - { - stream_ = stream; - } - - #endregion Constructors - - /// - /// Get / set a value indicating whether the underlying stream is owned or not. - /// - /// If the stream is owned it is closed when this instance is closed. - public bool IsStreamOwner - { - get { return isOwner_; } - set { isOwner_ = value; } - } - - #region Base Stream Methods - - public override bool CanRead - { - get { return stream_.CanRead; } - } - - public override bool CanSeek - { - get { return stream_.CanSeek; } - } - - public override bool CanTimeout - { - get { return stream_.CanTimeout; } - } - - public override long Length - { - get { return stream_.Length; } - } - - public override long Position - { - get { return stream_.Position; } - set { stream_.Position = value; } - } - - public override bool CanWrite - { - get { return stream_.CanWrite; } - } - - public override void Flush() - { - stream_.Flush(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - return stream_.Seek(offset, origin); - } - - public override void SetLength(long value) - { - stream_.SetLength(value); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return stream_.Read(buffer, offset, count); - } - - public override void Write(byte[] buffer, int offset, int count) - { - stream_.Write(buffer, offset, count); - } - - /// - /// Close the stream. - /// - /// - /// The underlying stream is closed only if is true. - /// - protected override void Dispose(bool disposing) - { - Stream toClose = stream_; - stream_ = null; - if (isOwner_ && (toClose != null)) - { - isOwner_ = false; - toClose.Dispose(); - } - } - - #endregion Base Stream Methods - - // Write the local file header - // TODO: ZipHelperStream.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage - private void WriteLocalHeader(ZipEntry entry, EntryPatchData patchData) - { - CompressionMethod method = entry.CompressionMethod; - bool headerInfoAvailable = true; // How to get this? - bool patchEntryHeader = false; - - WriteLEInt(ZipConstants.LocalHeaderSignature); - - WriteLEShort(entry.Version); - WriteLEShort(entry.Flags); - WriteLEShort((byte)method); - WriteLEInt((int)entry.DosTime); - - if (headerInfoAvailable == true) - { - WriteLEInt((int)entry.Crc); - if (entry.LocalHeaderRequiresZip64) - { - WriteLEInt(-1); - WriteLEInt(-1); - } - else - { - WriteLEInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); - WriteLEInt((int)entry.Size); - } - } - else - { - if (patchData != null) - { - patchData.CrcPatchOffset = stream_.Position; - } - WriteLEInt(0); // Crc - - if (patchData != null) - { - patchData.SizePatchOffset = stream_.Position; - } - - // For local header both sizes appear in Zip64 Extended Information - if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) - { - WriteLEInt(-1); - WriteLEInt(-1); - } - else - { - WriteLEInt(0); // Compressed size - WriteLEInt(0); // Uncompressed size - } - } - - byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); - - if (name.Length > 0xFFFF) - { - throw new ZipException("Entry name too long."); - } - - var ed = new ZipExtraData(entry.ExtraData); - - if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader)) - { - ed.StartNewEntry(); - if (headerInfoAvailable) - { - ed.AddLeLong(entry.Size); - ed.AddLeLong(entry.CompressedSize); - } - else - { - ed.AddLeLong(-1); - ed.AddLeLong(-1); - } - ed.AddNewEntry(1); - - if (!ed.Find(1)) - { - throw new ZipException("Internal error cant find extra data"); - } - - if (patchData != null) - { - patchData.SizePatchOffset = ed.CurrentReadIndex; - } - } - else - { - ed.Delete(1); - } - - byte[] extra = ed.GetEntryData(); - - WriteLEShort(name.Length); - WriteLEShort(extra.Length); - - if (name.Length > 0) - { - stream_.Write(name, 0, name.Length); - } - - if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) - { - patchData.SizePatchOffset += stream_.Position; - } - - if (extra.Length > 0) - { - stream_.Write(extra, 0, extra.Length); - } - } - - /// - /// Locates a block with the desired . - /// - /// The signature to find. - /// Location, marking the end of block. - /// Minimum size of the block. - /// The maximum variable data. - /// Returns the offset of the first byte after the signature; -1 if not found - public long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) - { - long pos = endLocation - minimumBlockSize; - if (pos < 0) - { - return -1; - } - - long giveUpMarker = Math.Max(pos - maximumVariableData, 0); - - // TODO: This loop could be optimised for speed. - do - { - if (pos < giveUpMarker) - { - return -1; - } - Seek(pos--, SeekOrigin.Begin); - } while (ReadLEInt() != signature); - - return Position; - } - - /// - /// Write Zip64 end of central directory records (File header and locator). - /// - /// The number of entries in the central directory. - /// The size of entries in the central directory. - /// The offset of the central directory. - public void WriteZip64EndOfCentralDirectory(long noOfEntries, long sizeEntries, long centralDirOffset) - { - long centralSignatureOffset = centralDirOffset + sizeEntries; - WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature); - WriteLELong(44); // Size of this record (total size of remaining fields in header or full size - 12) - WriteLEShort(ZipConstants.VersionMadeBy); // Version made by - WriteLEShort(ZipConstants.VersionZip64); // Version to extract - WriteLEInt(0); // Number of this disk - WriteLEInt(0); // number of the disk with the start of the central directory - WriteLELong(noOfEntries); // No of entries on this disk - WriteLELong(noOfEntries); // Total No of entries in central directory - WriteLELong(sizeEntries); // Size of the central directory - WriteLELong(centralDirOffset); // offset of start of central directory - // zip64 extensible data sector not catered for here (variable size) - - // Write the Zip64 end of central directory locator - WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature); - - // no of the disk with the start of the zip64 end of central directory - WriteLEInt(0); - - // relative offset of the zip64 end of central directory record - WriteLELong(centralSignatureOffset); - - // total number of disks - WriteLEInt(1); - } - - /// - /// Write Zip64 end of central directory records (File header and locator). - /// - /// The number of entries in the central directory. - /// The size of entries in the central directory. - /// The offset of the central directory. - /// The that can be used to cancel the operation. - public async Task WriteZip64EndOfCentralDirectoryAsync(long noOfEntries, long sizeEntries, long centralDirOffset, CancellationToken cancellationToken) - { - long centralSignatureOffset = centralDirOffset + sizeEntries; - await WriteLEIntAsync(ZipConstants.Zip64CentralFileHeaderSignature, cancellationToken); - await WriteLELongAsync(44, cancellationToken); // Size of this record (total size of remaining fields in header or full size - 12) - await WriteLEShortAsync(ZipConstants.VersionMadeBy, cancellationToken); // Version made by - await WriteLEShortAsync(ZipConstants.VersionZip64, cancellationToken); // Version to extract - await WriteLEIntAsync(0, cancellationToken); // Number of this disk - await WriteLEIntAsync(0, cancellationToken); // number of the disk with the start of the central directory - await WriteLELongAsync(noOfEntries, cancellationToken); // No of entries on this disk - await WriteLELongAsync(noOfEntries, cancellationToken); // Total No of entries in central directory - await WriteLELongAsync(sizeEntries, cancellationToken); // Size of the central directory - await WriteLELongAsync(centralDirOffset, cancellationToken); // offset of start of central directory - // zip64 extensible data sector not catered for here (variable size) - - // Write the Zip64 end of central directory locator - await WriteLEIntAsync(ZipConstants.Zip64CentralDirLocatorSignature, cancellationToken); - - // no of the disk with the start of the zip64 end of central directory - await WriteLEIntAsync(0, cancellationToken); - - // relative offset of the zip64 end of central directory record - await WriteLELongAsync(centralSignatureOffset, cancellationToken); - - // total number of disks - await WriteLEIntAsync(1, cancellationToken); - } - - /// - /// Write the required records to end the central directory. - /// - /// The number of entries in the directory. - /// The size of the entries in the directory. - /// The start of the central directory. - /// The archive comment. (This can be null). - public void WriteEndOfCentralDirectory(long noOfEntries, long sizeEntries, - long startOfCentralDirectory, byte[] comment) - { - if ((noOfEntries >= 0xffff) || - (startOfCentralDirectory >= 0xffffffff) || - (sizeEntries >= 0xffffffff)) - { - WriteZip64EndOfCentralDirectory(noOfEntries, sizeEntries, startOfCentralDirectory); - } - - WriteLEInt(ZipConstants.EndOfCentralDirectorySignature); - - // TODO: ZipFile Multi disk handling not done - WriteLEShort(0); // number of this disk - WriteLEShort(0); // no of disk with start of central dir - - // Number of entries - if (noOfEntries >= 0xffff) - { - WriteLEUshort(0xffff); // Zip64 marker - WriteLEUshort(0xffff); - } - else - { - WriteLEShort((short)noOfEntries); // entries in central dir for this disk - WriteLEShort((short)noOfEntries); // total entries in central directory - } - - // Size of the central directory - if (sizeEntries >= 0xffffffff) - { - WriteLEUint(0xffffffff); // Zip64 marker - } - else - { - WriteLEInt((int)sizeEntries); - } - - // offset of start of central directory - if (startOfCentralDirectory >= 0xffffffff) - { - WriteLEUint(0xffffffff); // Zip64 marker - } - else - { - WriteLEInt((int)startOfCentralDirectory); - } - - int commentLength = (comment != null) ? comment.Length : 0; - - if (commentLength > 0xffff) - { - throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength)); - } - - WriteLEShort(commentLength); - - if (commentLength > 0) - { - Write(comment, 0, comment.Length); - } - } - - /// - /// Write the required records to end the central directory. - /// - /// The number of entries in the directory. - /// The size of the entries in the directory. - /// The start of the central directory. - /// The archive comment. (This can be null). - /// The that can be used to cancel the operation. - public async Task WriteEndOfCentralDirectoryAsync(long noOfEntries, long sizeEntries, - long startOfCentralDirectory, byte[] comment, CancellationToken cancellationToken) - { - if ((noOfEntries >= 0xffff) || - (startOfCentralDirectory >= 0xffffffff) || - (sizeEntries >= 0xffffffff)) - { - await WriteZip64EndOfCentralDirectoryAsync(noOfEntries, sizeEntries, startOfCentralDirectory, cancellationToken); - } - - await WriteLEIntAsync(ZipConstants.EndOfCentralDirectorySignature, cancellationToken); - - // TODO: ZipFile Multi disk handling not done - await WriteLEShortAsync(0, cancellationToken); // number of this disk - await WriteLEShortAsync(0, cancellationToken); // no of disk with start of central dir - - // Number of entries - if (noOfEntries >= 0xffff) - { - await WriteLEUshortAsync(0xffff, cancellationToken); // Zip64 marker - await WriteLEUshortAsync(0xffff, cancellationToken); - } - else - { - await WriteLEShortAsync((short)noOfEntries, cancellationToken); // entries in central dir for this disk - await WriteLEShortAsync((short)noOfEntries, cancellationToken); // total entries in central directory - } - - // Size of the central directory - if (sizeEntries >= 0xffffffff) - { - await WriteLEUintAsync(0xffffffff, cancellationToken); // Zip64 marker - } - else - { - await WriteLEIntAsync((int)sizeEntries, cancellationToken); - } - - // offset of start of central directory - if (startOfCentralDirectory >= 0xffffffff) - { - await WriteLEUintAsync(0xffffffff, cancellationToken); // Zip64 marker - } - else - { - await WriteLEIntAsync((int)startOfCentralDirectory, cancellationToken); - } - - int commentLength = (comment != null) ? comment.Length : 0; - - if (commentLength > 0xffff) - { - throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength)); - } - - await WriteLEShortAsync(commentLength, cancellationToken); - - if (commentLength > 0) - { - await WriteAsync(comment, 0, comment.Length, cancellationToken); - } - } - - #region LE value reading/writing - - /// - /// Read an unsigned short in little endian byte order. - /// - /// Returns the value read. - /// - /// An i/o error occurs. - /// - /// - /// The file ends prematurely - /// - public int ReadLEShort() - { - int byteValue1 = stream_.ReadByte(); - - if (byteValue1 < 0) - { - throw new EndOfStreamException(); - } - - int byteValue2 = stream_.ReadByte(); - if (byteValue2 < 0) - { - throw new EndOfStreamException(); - } - - return byteValue1 | (byteValue2 << 8); - } - - /// - /// Read an int in little endian byte order. - /// - /// Returns the value read. - /// - /// An i/o error occurs. - /// - /// - /// The file ends prematurely - /// - public int ReadLEInt() - { - return ReadLEShort() | (ReadLEShort() << 16); - } - - /// - /// Read a long in little endian byte order. - /// - /// The value read. - public long ReadLELong() - { - return (uint)ReadLEInt() | ((long)ReadLEInt() << 32); - } - - /// - /// Write an unsigned short in little endian byte order. - /// - /// The value to write. - public void WriteLEShort(int value) - { - stream_.WriteByte((byte)(value & 0xff)); - stream_.WriteByte((byte)((value >> 8) & 0xff)); - } - - /// - /// Write an unsigned short in little endian byte order. - /// - /// The value to write. - /// The that can be used to cancel the operation. - public async Task WriteLEShortAsync(int value, CancellationToken cancellationToken) - { - await stream_.WriteAsync(new[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff) }, 0, 2, cancellationToken); - } - - /// - /// Write a ushort in little endian byte order. - /// - /// The value to write. - public void WriteLEUshort(ushort value) - { - stream_.WriteByte((byte)(value & 0xff)); - stream_.WriteByte((byte)(value >> 8)); - } - - /// - /// Write a ushort in little endian byte order. - /// - /// The value to write. - /// The that can be used to cancel the operation. - public async Task WriteLEUshortAsync(ushort value, CancellationToken cancellationToken) - { - await stream_.WriteAsync(new[] { (byte)(value & 0xff), (byte)(value >> 8) }, 0, 2, cancellationToken); - } - - /// - /// Write an int in little endian byte order. - /// - /// The value to write. - public void WriteLEInt(int value) - { - WriteLEShort(value); - WriteLEShort(value >> 16); - } - - /// - /// Write an int in little endian byte order. - /// - /// The value to write. - /// The that can be used to cancel the operation. - public async Task WriteLEIntAsync(int value, CancellationToken cancellationToken) - { - await WriteLEShortAsync(value, cancellationToken); - await WriteLEShortAsync(value >> 16, cancellationToken); - } - - /// - /// Write a uint in little endian byte order. - /// - /// The value to write. - public void WriteLEUint(uint value) - { - WriteLEUshort((ushort)(value & 0xffff)); - WriteLEUshort((ushort)(value >> 16)); - } - - /// - /// Write a uint in little endian byte order. - /// - /// The value to write. - /// The that can be used to cancel the operation. - public async Task WriteLEUintAsync(uint value, CancellationToken cancellationToken) - { - await WriteLEUshortAsync((ushort)(value & 0xffff), cancellationToken); - await WriteLEUshortAsync((ushort)(value >> 16), cancellationToken); - } - - /// - /// Write a long in little endian byte order. - /// - /// The value to write. - public void WriteLELong(long value) - { - WriteLEInt((int)value); - WriteLEInt((int)(value >> 32)); - } - - /// - /// Write a long in little endian byte order. - /// - /// The value to write. - /// The that can be used to cancel the operation. - public async Task WriteLELongAsync(long value, CancellationToken cancellationToken) - { - await WriteLEIntAsync((int)value, cancellationToken); - await WriteLEIntAsync((int)(value >> 32), cancellationToken); - } - - /// - /// Write a ulong in little endian byte order. - /// - /// The value to write. - public void WriteLEUlong(ulong value) - { - WriteLEUint((uint)(value & 0xffffffff)); - WriteLEUint((uint)(value >> 32)); - } - - /// - /// Write a ulong in little endian byte order. - /// - /// The value to write. - /// The that can be used to cancel the operation. - public async Task WriteLEUlongAsync(ulong value, CancellationToken cancellationToken) - { - await WriteLEUintAsync((uint)(value & 0xffffffff), cancellationToken); - await WriteLEUintAsync((uint)(value >> 32), cancellationToken); - } - - #endregion LE value reading/writing - - /// - /// Write a data descriptor. - /// - /// The entry to write a descriptor for. - /// Returns the number of descriptor bytes written. - public int WriteDataDescriptor(ZipEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - int result = 0; - - // Add data descriptor if flagged as required - if ((entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) - { - // The signature is not PKZIP originally but is now described as optional - // in the PKZIP Appnote documenting trhe format. - WriteLEInt(ZipConstants.DataDescriptorSignature); - WriteLEInt(unchecked((int)(entry.Crc))); - - result += 8; - - if (entry.LocalHeaderRequiresZip64) - { - WriteLELong(entry.CompressedSize); - WriteLELong(entry.Size); - result += 16; - } - else - { - WriteLEInt((int)entry.CompressedSize); - WriteLEInt((int)entry.Size); - result += 8; - } - } - - return result; - } - - /// - /// Read data descriptor at the end of compressed data. - /// - /// if set to true [zip64]. - /// The data to fill in. - /// Returns the number of bytes read in the descriptor. - public void ReadDataDescriptor(bool zip64, DescriptorData data) - { - int intValue = ReadLEInt(); - - // In theory this may not be a descriptor according to PKZIP appnote. - // In practise its always there. - if (intValue != ZipConstants.DataDescriptorSignature) - { - throw new ZipException("Data descriptor signature not found"); - } - - data.Crc = ReadLEInt(); - - if (zip64) - { - data.CompressedSize = ReadLELong(); - data.Size = ReadLELong(); - } - else - { - data.CompressedSize = ReadLEInt(); - data.Size = ReadLEInt(); - } - } - - #region Instance Fields - - private bool isOwner_; - private Stream stream_; - - #endregion Instance Fields - } -} diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index a0d9886bf..f58877920 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -155,75 +155,7 @@ public UseZip64 UseZip64 /// Defaults to , set to null to disable transforms and use names as supplied. /// public INameTransform NameTransform { get; set; } = new PathTransformer(); - - /// - /// Write an unsigned short in little endian byte order. - /// - private void WriteLeShort(int value) - { - unchecked - { - baseOutputStream_.WriteByte((byte)(value & 0xff)); - baseOutputStream_.WriteByte((byte)((value >> 8) & 0xff)); - } - } - - /// - /// Write an unsigned short in little endian byte order. - /// - private async Task WriteLeShortAsync(int value, CancellationToken cancellationToken) - { - unchecked - { - await baseOutputStream_.WriteAsync(new[] {(byte)(value & 0xff), (byte)((value >> 8) & 0xff)}, 0, 2, cancellationToken); - } - } - - /// - /// Write an int in little endian byte order. - /// - private void WriteLeInt(int value) - { - unchecked - { - WriteLeShort(value); - WriteLeShort(value >> 16); - } - } - - /// - /// Write an int in little endian byte order. - /// - private async Task WriteLeIntAsync(int value, CancellationToken cancellationToken) - { - await WriteLeShortAsync(value, cancellationToken); - await WriteLeShortAsync(value >> 16, cancellationToken); - } - - /// - /// Write an int in little endian byte order. - /// - private void WriteLeLong(long value) - { - unchecked - { - WriteLeInt((int)value); - WriteLeInt((int)(value >> 32)); - } - } - - /// - /// Write an int in little endian byte order. - /// - private async Task WriteLeLongAsync(long value, CancellationToken cancellationToken) - { - unchecked - { - await WriteLeIntAsync((int)value, cancellationToken); - await WriteLeIntAsync((int)(value >> 32), cancellationToken); - } - } - + // Apply any configured transforms/cleaning to the name of the supplied entry. private void TransformEntryName(ZipEntry entry) { @@ -254,7 +186,7 @@ private void TransformEntryName(ZipEntry entry) /// if entry passed is null. /// /// - /// if an I/O error occured. + /// if an I/O error occurred. /// /// /// if stream was finished @@ -268,6 +200,9 @@ private void TransformEntryName(ZipEntry entry) /// The Compression method specified for the entry is unsupported. /// public void PutNextEntry(ZipEntry entry) + => PutNextEntry(baseOutputStream_, entry); + + internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) { if (entry == null) { @@ -383,51 +318,51 @@ public void PutNextEntry(ZipEntry entry) } // Write the local file header - WriteLeInt(ZipConstants.LocalHeaderSignature); + stream.WriteLEInt(ZipConstants.LocalHeaderSignature); - WriteLeShort(entry.Version); - WriteLeShort(entry.Flags); - WriteLeShort((byte)entry.CompressionMethodForHeader); - WriteLeInt((int)entry.DosTime); + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((byte)entry.CompressionMethodForHeader); + stream.WriteLEInt((int)entry.DosTime); // TODO: Refactor header writing. Its done in several places. if (headerInfoAvailable) { - WriteLeInt((int)entry.Crc); + stream.WriteLEInt((int)entry.Crc); if (entry.LocalHeaderRequiresZip64) { - WriteLeInt(-1); - WriteLeInt(-1); + stream.WriteLEInt(-1); + stream.WriteLEInt(-1); } else { - WriteLeInt((int)entry.CompressedSize + entry.EncryptionOverheadSize); - WriteLeInt((int)entry.Size); + stream.WriteLEInt((int)entry.CompressedSize + entry.EncryptionOverheadSize); + stream.WriteLEInt((int)entry.Size); } } else { if (patchEntryHeader) { - crcPatchPos = baseOutputStream_.Position; + crcPatchPos = stream.Position + streamOffset; } - WriteLeInt(0); // Crc + stream.WriteLEInt(0); // Crc if (patchEntryHeader) { - sizePatchPos = baseOutputStream_.Position; + sizePatchPos = stream.Position + streamOffset; } // For local header both sizes appear in Zip64 Extended Information if (entry.LocalHeaderRequiresZip64 || patchEntryHeader) { - WriteLeInt(-1); - WriteLeInt(-1); + stream.WriteLEInt(-1); + stream.WriteLEInt(-1); } else { - WriteLeInt(0); // Compressed size - WriteLeInt(0); // Uncompressed size + stream.WriteLEInt(0); // Compressed size + stream.WriteLEInt(0); // Uncompressed size } } @@ -478,8 +413,8 @@ public void PutNextEntry(ZipEntry entry) } byte[] extra = ed.GetEntryData(); - WriteLeShort(name.Length); - WriteLeShort(extra.Length); + stream.WriteLEShort(name.Length); + stream.WriteLEShort(extra.Length); if (name.Length > 0) { @@ -488,7 +423,7 @@ public void PutNextEntry(ZipEntry entry) if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) { - sizePatchPos += baseOutputStream_.Position; + sizePatchPos += stream.Position + streamOffset; } if (extra.Length > 0) @@ -561,530 +496,182 @@ public void PutNextEntry(ZipEntry entry) /// public async Task PutNextEntryAsync(ZipEntry entry, CancellationToken cancellationToken = default) { - if (entry == null) + using (var ms = new MemoryStream()) { - throw new ArgumentNullException(nameof(entry)); + PutNextEntry(ms, entry, baseOutputStream_.Position); + await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); } + } - if (entries == null) - { - throw new InvalidOperationException("ZipOutputStream was finished"); - } + /// + /// Closes the current entry, updating header and footer information as required + /// + /// + /// An I/O error occurs. + /// + /// + /// No entry is active. + /// + public void CloseEntry() + { + CloseEntry(baseOutputStream_); - if (curEntry != null) + // Patch the header if possible + if (patchEntryHeader) { - await CloseEntryAsync(cancellationToken); - } + patchEntryHeader = false; - if (entries.Count == int.MaxValue) - { - throw new ZipException("Too many entries for Zip file"); + long curPos = baseOutputStream_.Position; + baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); + PatchLocalHeader(baseOutputStream_); + baseOutputStream_.Seek(curPos, SeekOrigin.Begin); } - CompressionMethod method = entry.CompressionMethod; - - // Check that the compression is one that we support - if (method != CompressionMethod.Deflated && method != CompressionMethod.Stored) - { - throw new NotImplementedException("Compression method not supported"); - } + entries.Add(curEntry); + curEntry = null; + } - // A password must have been set in order to add AES encrypted entries - if (entry.AESKeySize > 0 && string.IsNullOrEmpty(this.Password)) + /// + public async Task CloseEntryAsync(CancellationToken cancellationToken) + { + using (var ms = new MemoryStream()) { - throw new InvalidOperationException("The Password property must be set before AES encrypted entries can be added"); - } + CloseEntry(ms); + await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); + + // Patch the header if possible + if (patchEntryHeader) + { + ms.Position = 0; + ms.SetLength(0); + PatchLocalHeader(ms); - int compressionLevel = defaultCompressionLevel; + patchEntryHeader = false; - // Clear flags that the library manages internally - entry.Flags &= (int)GeneralBitFlags.UnicodeText; - patchEntryHeader = false; + long curPos = baseOutputStream_.Position; + baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); + await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); + baseOutputStream_.Seek(curPos, SeekOrigin.Begin); + } + } - bool headerInfoAvailable; + entries.Add(curEntry); + curEntry = null; + } - // No need to compress - definitely no data. - if (entry.Size == 0) + internal void CloseEntry(Stream stream) + { + if (curEntry == null) { - entry.CompressedSize = entry.Size; - entry.Crc = 0; - method = CompressionMethod.Stored; - headerInfoAvailable = true; + throw new InvalidOperationException("No open entry"); } - else - { - headerInfoAvailable = (entry.Size >= 0) && entry.HasCrc && entry.CompressedSize >= 0; - // Switch to deflation if storing isnt possible. - if (method == CompressionMethod.Stored) - { - if (!headerInfoAvailable) - { - if (!CanPatchEntries) - { - // Can't patch entries so storing is not possible. - method = CompressionMethod.Deflated; - compressionLevel = 0; - } - } - else // entry.size must be > 0 - { - entry.CompressedSize = entry.Size; - headerInfoAvailable = entry.HasCrc; - } - } - } + long csize = size; - if (headerInfoAvailable == false) + // First finish the deflater, if appropriate + if (curMethod == CompressionMethod.Deflated) { - if (CanPatchEntries == false) + if (size >= 0) { - // Only way to record size and compressed size is to append a data descriptor - // after compressed data. - - // Stored entries of this form have already been converted to deflating. - entry.Flags |= 8; + base.Finish(); + csize = deflater_.TotalOut; } else { - patchEntryHeader = true; + deflater_.Reset(); } } - - if (Password != null) + else if (curMethod == CompressionMethod.Stored) { - entry.IsCrypted = true; - if (entry.Crc < 0) - { - // Need to append a data descriptor as the crc isnt available for use - // with encryption, the date is used instead. Setting the flag - // indicates this to the decompressor. - entry.Flags |= 8; - } + // This is done by Finsh() for Deflated entries, but we need to do it + // ourselves for Stored ones + base.GetAuthCodeIfAES(); } - entry.Offset = offset; - entry.CompressionMethod = (CompressionMethod)method; - - curMethod = method; - sizePatchPos = -1; - - if ((useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic))) + // Write the AES Authentication Code (a hash of the compressed and encrypted data) + if (curEntry.AESKeySize > 0) { - entry.ForceZip64(); + stream.Write(AESAuthCode, 0, 10); } - // Write the local file header - await WriteLeIntAsync(ZipConstants.LocalHeaderSignature, cancellationToken); - - await WriteLeShortAsync(entry.Version, cancellationToken); - await WriteLeShortAsync(entry.Flags, cancellationToken); - await WriteLeShortAsync((byte)entry.CompressionMethodForHeader, cancellationToken); - await WriteLeIntAsync((int)entry.DosTime, cancellationToken); - - // TODO: Refactor header writing. Its done in several places. - if (headerInfoAvailable) + if (curEntry.Size < 0) { - await WriteLeIntAsync((int)entry.Crc, cancellationToken); - if (entry.LocalHeaderRequiresZip64) - { - await WriteLeIntAsync(-1, cancellationToken); - await WriteLeIntAsync(-1, cancellationToken); - } - else - { - await WriteLeIntAsync((int)entry.CompressedSize + entry.EncryptionOverheadSize, cancellationToken); - await WriteLeIntAsync((int)entry.Size, cancellationToken); - } + curEntry.Size = size; } - else + else if (curEntry.Size != size) { - if (patchEntryHeader) - { - crcPatchPos = baseOutputStream_.Position; - } - await WriteLeIntAsync(0, cancellationToken); // Crc + throw new ZipException("size was " + size + ", but I expected " + curEntry.Size); + } - if (patchEntryHeader) - { - sizePatchPos = baseOutputStream_.Position; - } + if (curEntry.CompressedSize < 0) + { + curEntry.CompressedSize = csize; + } + else if (curEntry.CompressedSize != csize) + { + throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize); + } - // For local header both sizes appear in Zip64 Extended Information - if (entry.LocalHeaderRequiresZip64 || patchEntryHeader) - { - await WriteLeIntAsync(-1, cancellationToken); - await WriteLeIntAsync(-1, cancellationToken); - } - else - { - await WriteLeIntAsync(0, cancellationToken); // Compressed size - await WriteLeIntAsync(0, cancellationToken); // Uncompressed size - } + if (curEntry.Crc < 0) + { + curEntry.Crc = crc.Value; + } + else if (curEntry.Crc != crc.Value) + { + throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc); } - // Apply any required transforms to the entry name, and then convert to byte array format. - TransformEntryName(entry); - byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); + offset += csize; - if (name.Length > 0xFFFF) + if (curEntry.IsCrypted) { - throw new ZipException("Entry name too long."); + curEntry.CompressedSize += curEntry.EncryptionOverheadSize; } - var ed = new ZipExtraData(entry.ExtraData); - - if (entry.LocalHeaderRequiresZip64) + // Add data descriptor if flagged as required + if ((curEntry.Flags & 8) != 0) { - ed.StartNewEntry(); - if (headerInfoAvailable) + stream.WriteLEInt(ZipConstants.DataDescriptorSignature); + stream.WriteLEInt(unchecked((int)curEntry.Crc)); + + if (curEntry.LocalHeaderRequiresZip64) { - ed.AddLeLong(entry.Size); - ed.AddLeLong(entry.CompressedSize + entry.EncryptionOverheadSize); + stream.WriteLELong(curEntry.CompressedSize); + stream.WriteLELong(curEntry.Size); + offset += ZipConstants.Zip64DataDescriptorSize; } else { - ed.AddLeLong(-1); - ed.AddLeLong(-1); + stream.WriteLEInt((int)curEntry.CompressedSize); + stream.WriteLEInt((int)curEntry.Size); + offset += ZipConstants.DataDescriptorSize; } - ed.AddNewEntry(1); + } + } - if (!ed.Find(1)) - { - throw new ZipException("Internal error cant find extra data"); - } + private void PatchLocalHeader(Stream stream) + { + stream.WriteLEInt((int)curEntry.Crc); - if (patchEntryHeader) + if (curEntry.LocalHeaderRequiresZip64) + { + if (sizePatchPos == -1) { - sizePatchPos = ed.CurrentReadIndex; + throw new ZipException("Entry requires zip64 but this has been turned off"); } + + baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); + stream.WriteLELong(curEntry.Size); + stream.WriteLELong(curEntry.CompressedSize); } else { - ed.Delete(1); + stream.WriteLEInt((int)curEntry.CompressedSize); + stream.WriteLEInt((int)curEntry.Size); } + } - if (entry.AESKeySize > 0) - { - AddExtraDataAES(entry, ed); - } - byte[] extra = ed.GetEntryData(); - - await WriteLeShortAsync(name.Length, cancellationToken); - await WriteLeShortAsync(extra.Length, cancellationToken); - - if (name.Length > 0) - { - await baseOutputStream_.WriteAsync(name, 0, name.Length, cancellationToken); - } - - if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) - { - sizePatchPos += baseOutputStream_.Position; - } - - if (extra.Length > 0) - { - await baseOutputStream_.WriteAsync(extra, 0, extra.Length, cancellationToken); - } - - offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; - // Fix offsetOfCentraldir for AES - if (entry.AESKeySize > 0) - offset += entry.AESOverheadSize; - - // Activate the entry. - curEntry = entry; - crc.Reset(); - if (method == CompressionMethod.Deflated) - { - deflater_.Reset(); - deflater_.SetLevel(compressionLevel); - } - size = 0; - - if (entry.IsCrypted) - { - if (entry.AESKeySize > 0) - { - await WriteAESHeaderAsync(entry, cancellationToken); - } - else - { - if (entry.Crc < 0) - { // so testing Zip will says its ok - await WriteEncryptionHeaderAsync(entry.DosTime << 16, cancellationToken); - } - else - { - await WriteEncryptionHeaderAsync(entry.Crc, cancellationToken); - } - } - } - } - - /// - /// Closes the current entry, updating header and footer information as required - /// - /// - /// An I/O error occurs. - /// - /// - /// No entry is active. - /// - public void CloseEntry() - { - if (curEntry == null) - { - throw new InvalidOperationException("No open entry"); - } - - long csize = size; - - // First finish the deflater, if appropriate - if (curMethod == CompressionMethod.Deflated) - { - if (size >= 0) - { - base.Finish(); - csize = deflater_.TotalOut; - } - else - { - deflater_.Reset(); - } - } - else if (curMethod == CompressionMethod.Stored) - { - // This is done by Finsh() for Deflated entries, but we need to do it - // ourselves for Stored ones - base.GetAuthCodeIfAES(); - } - - // Write the AES Authentication Code (a hash of the compressed and encrypted data) - if (curEntry.AESKeySize > 0) - { - baseOutputStream_.Write(AESAuthCode, 0, 10); - } - - if (curEntry.Size < 0) - { - curEntry.Size = size; - } - else if (curEntry.Size != size) - { - throw new ZipException("size was " + size + ", but I expected " + curEntry.Size); - } - - if (curEntry.CompressedSize < 0) - { - curEntry.CompressedSize = csize; - } - else if (curEntry.CompressedSize != csize) - { - throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize); - } - - if (curEntry.Crc < 0) - { - curEntry.Crc = crc.Value; - } - else if (curEntry.Crc != crc.Value) - { - throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc); - } - - offset += csize; - - if (curEntry.IsCrypted) - { - curEntry.CompressedSize += curEntry.EncryptionOverheadSize; - } - - // Patch the header if possible - if (patchEntryHeader) - { - patchEntryHeader = false; - - long curPos = baseOutputStream_.Position; - baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); - WriteLeInt((int)curEntry.Crc); - - if (curEntry.LocalHeaderRequiresZip64) - { - if (sizePatchPos == -1) - { - throw new ZipException("Entry requires zip64 but this has been turned off"); - } - - baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); - WriteLeLong(curEntry.Size); - WriteLeLong(curEntry.CompressedSize); - } - else - { - WriteLeInt((int)curEntry.CompressedSize); - WriteLeInt((int)curEntry.Size); - } - baseOutputStream_.Seek(curPos, SeekOrigin.Begin); - } - - // Add data descriptor if flagged as required - if ((curEntry.Flags & 8) != 0) - { - WriteLeInt(ZipConstants.DataDescriptorSignature); - WriteLeInt(unchecked((int)curEntry.Crc)); - - if (curEntry.LocalHeaderRequiresZip64) - { - WriteLeLong(curEntry.CompressedSize); - WriteLeLong(curEntry.Size); - offset += ZipConstants.Zip64DataDescriptorSize; - } - else - { - WriteLeInt((int)curEntry.CompressedSize); - WriteLeInt((int)curEntry.Size); - offset += ZipConstants.DataDescriptorSize; - } - } - - entries.Add(curEntry); - curEntry = null; - } - - /// - /// Closes the current entry, updating header and footer information as required - /// - /// The that can be used to cancel the operation. - /// - /// An I/O error occurs. - /// - /// - /// No entry is active. - /// - public async Task CloseEntryAsync(CancellationToken cancellationToken) - { - if (curEntry == null) - { - throw new InvalidOperationException("No open entry"); - } - - long csize = size; - - // First finish the deflater, if appropriate - if (curMethod == CompressionMethod.Deflated) - { - if (size >= 0) - { - await base.FinishAsync(cancellationToken); - csize = deflater_.TotalOut; - } - else - { - deflater_.Reset(); - } - } - else if (curMethod == CompressionMethod.Stored) - { - // This is done by Finsh() for Deflated entries, but we need to do it - // ourselves for Stored ones - base.GetAuthCodeIfAES(); - } - - // Write the AES Authentication Code (a hash of the compressed and encrypted data) - if (curEntry.AESKeySize > 0) - { - await baseOutputStream_.WriteAsync(AESAuthCode, 0, 10, cancellationToken); - } - - if (curEntry.Size < 0) - { - curEntry.Size = size; - } - else if (curEntry.Size != size) - { - throw new ZipException("size was " + size + ", but I expected " + curEntry.Size); - } - - if (curEntry.CompressedSize < 0) - { - curEntry.CompressedSize = csize; - } - else if (curEntry.CompressedSize != csize) - { - throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize); - } - - if (curEntry.Crc < 0) - { - curEntry.Crc = crc.Value; - } - else if (curEntry.Crc != crc.Value) - { - throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc); - } - - offset += csize; - - if (curEntry.IsCrypted) - { - curEntry.CompressedSize += curEntry.EncryptionOverheadSize; - } - - // Patch the header if possible - if (patchEntryHeader) - { - patchEntryHeader = false; - - long curPos = baseOutputStream_.Position; - baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); - await WriteLeIntAsync((int)curEntry.Crc, cancellationToken); - - if (curEntry.LocalHeaderRequiresZip64) - { - if (sizePatchPos == -1) - { - throw new ZipException("Entry requires zip64 but this has been turned off"); - } - - baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); - await WriteLeLongAsync(curEntry.Size, cancellationToken); - await WriteLeLongAsync(curEntry.CompressedSize, cancellationToken); - } - else - { - await WriteLeIntAsync((int)curEntry.CompressedSize, cancellationToken); - await WriteLeIntAsync((int)curEntry.Size, cancellationToken); - } - baseOutputStream_.Seek(curPos, SeekOrigin.Begin); - } - - // Add data descriptor if flagged as required - if ((curEntry.Flags & 8) != 0) - { - await WriteLeIntAsync(ZipConstants.DataDescriptorSignature, cancellationToken); - await WriteLeIntAsync(unchecked((int)curEntry.Crc), cancellationToken); - - if (curEntry.LocalHeaderRequiresZip64) - { - await WriteLeLongAsync(curEntry.CompressedSize, cancellationToken); - await WriteLeLongAsync(curEntry.Size, cancellationToken); - offset += ZipConstants.Zip64DataDescriptorSize; - } - else - { - await WriteLeIntAsync((int)curEntry.CompressedSize, cancellationToken); - await WriteLeIntAsync((int)curEntry.Size, cancellationToken); - offset += ZipConstants.DataDescriptorSize; - } - } - - entries.Add(curEntry); - curEntry = null; - } - - private void WriteEncryptionHeader(long crcValue) + private byte[] CreateEncryptionHeader(long crcValue) { offset += ZipConstants.CryptoHeaderSize; @@ -1099,24 +686,19 @@ private void WriteEncryptionHeader(long crcValue) cryptBuffer[11] = (byte)(crcValue >> 24); EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); + + return cryptBuffer; + } + + private void WriteEncryptionHeader(long crcValue) + { + var cryptBuffer = CreateEncryptionHeader(crcValue); baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length); } private async Task WriteEncryptionHeaderAsync(long crcValue, CancellationToken cancellationToken) { - offset += ZipConstants.CryptoHeaderSize; - - InitializePassword(Password); - - byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; - using (var rng = new RNGCryptoServiceProvider()) - { - rng.GetBytes(cryptBuffer); - } - - cryptBuffer[11] = (byte)(crcValue >> 24); - - EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); + var cryptBuffer = CreateEncryptionHeader(crcValue); await baseOutputStream_.WriteAsync(cryptBuffer, 0, cryptBuffer.Length, cancellationToken); } @@ -1136,13 +718,9 @@ private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) extraData.AddNewEntry(0x9901); } - // Replaces WriteEncryptionHeader for AES - // - private void WriteAESHeader(ZipEntry entry) + private byte[] CreateAESHeader(ZipEntry entry) { - byte[] salt; - byte[] pwdVerifier; - InitializeAESPassword(entry, Password, out salt, out pwdVerifier); + InitializeAESPassword(entry, Password, out var salt, out var pwdVerifier); // File format for AES: // Size (bytes) Content // ------------ ------- @@ -1154,30 +732,21 @@ private void WriteAESHeader(ZipEntry entry) // Value in the "compressed size" fields of the local file header and the central directory entry // is the total size of all the items listed above. In other words, it is the total size of the // salt value, password verification value, encrypted data, and authentication code. - baseOutputStream_.Write(salt, 0, salt.Length); - baseOutputStream_.Write(pwdVerifier, 0, pwdVerifier.Length); + Array.Resize(ref salt, salt.Length + 2); + Array.Copy(pwdVerifier, 0, salt, salt.Length - 2, 2); + return salt; + } + + private void WriteAESHeader(ZipEntry entry) + { + var header = CreateAESHeader(entry); + baseOutputStream_.Write(header, 0, header.Length); } - // Replaces WriteEncryptionHeader for AES - // private async Task WriteAESHeaderAsync(ZipEntry entry, CancellationToken cancellationToken) { - byte[] salt; - byte[] pwdVerifier; - InitializeAESPassword(entry, Password, out salt, out pwdVerifier); - // File format for AES: - // Size (bytes) Content - // ------------ ------- - // Variable Salt value - // 2 Password verification value - // Variable Encrypted file data - // 10 Authentication code - // - // Value in the "compressed size" fields of the local file header and the central directory entry - // is the total size of all the items listed above. In other words, it is the total size of the - // salt value, password verification value, encrypted data, and authentication code. - await baseOutputStream_.WriteAsync(salt, 0, salt.Length, cancellationToken); - await baseOutputStream_.WriteAsync(pwdVerifier, 0, pwdVerifier.Length, cancellationToken); + var header = CreateAESHeader(entry); + await baseOutputStream_.WriteAsync(header, 0, header.Length, cancellationToken); } /// @@ -1282,317 +851,178 @@ public override void Finish() long numEntries = entries.Count; long sizeEntries = 0; - foreach (ZipEntry entry in entries) + foreach (var entry in entries) { - WriteLeInt(ZipConstants.CentralHeaderSignature); - WriteLeShort((entry.HostSystem << 8) | entry.VersionMadeBy); - WriteLeShort(entry.Version); - WriteLeShort(entry.Flags); - WriteLeShort((short)entry.CompressionMethodForHeader); - WriteLeInt((int)entry.DosTime); - WriteLeInt((int)entry.Crc); - - if (entry.IsZip64Forced() || - (entry.CompressedSize >= uint.MaxValue)) - { - WriteLeInt(-1); - } - else - { - WriteLeInt((int)entry.CompressedSize); - } - - if (entry.IsZip64Forced() || - (entry.Size >= uint.MaxValue)) - { - WriteLeInt(-1); - } - else - { - WriteLeInt((int)entry.Size); - } - - byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); - - if (name.Length > 0xffff) - { - throw new ZipException("Name too long."); - } - - var ed = new ZipExtraData(entry.ExtraData); - - if (entry.CentralHeaderRequiresZip64) - { - ed.StartNewEntry(); - if (entry.IsZip64Forced() || - (entry.Size >= 0xffffffff)) - { - ed.AddLeLong(entry.Size); - } - - if (entry.IsZip64Forced() || - (entry.CompressedSize >= 0xffffffff)) - { - ed.AddLeLong(entry.CompressedSize); - } - - if (entry.Offset >= 0xffffffff) - { - ed.AddLeLong(entry.Offset); - } - - ed.AddNewEntry(1); - } - else - { - ed.Delete(1); - } - - if (entry.AESKeySize > 0) - { - AddExtraDataAES(entry, ed); - } - byte[] extra = ed.GetEntryData(); - - byte[] entryComment = - (entry.Comment != null) ? - ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : - new byte[0]; - - if (entryComment.Length > 0xffff) - { - throw new ZipException("Comment too long."); - } - - WriteLeShort(name.Length); - WriteLeShort(extra.Length); - WriteLeShort(entryComment.Length); - WriteLeShort(0); // disk number - WriteLeShort(0); // internal file attributes - // external file attributes - - if (entry.ExternalFileAttributes != -1) - { - WriteLeInt(entry.ExternalFileAttributes); - } - else - { - if (entry.IsDirectory) - { // mark entry as directory (from nikolam.AT.perfectinfo.com) - WriteLeInt(16); - } - else - { - WriteLeInt(0); - } - } - - if (entry.Offset >= uint.MaxValue) - { - WriteLeInt(-1); - } - else - { - WriteLeInt((int)entry.Offset); - } - - if (name.Length > 0) - { - baseOutputStream_.Write(name, 0, name.Length); - } - - if (extra.Length > 0) - { - baseOutputStream_.Write(extra, 0, extra.Length); - } - - if (entryComment.Length > 0) - { - baseOutputStream_.Write(entryComment, 0, entryComment.Length); - } - - sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + sizeEntries += WriteEndEntry(baseOutputStream_, entry); } - using (ZipHelperStream zhs = new ZipHelperStream(baseOutputStream_)) - { - zhs.WriteEndOfCentralDirectory(numEntries, sizeEntries, offset, zipComment); - } + ZipFormat.WriteEndOfCentralDirectory(baseOutputStream_, numEntries, sizeEntries, offset, zipComment); entries = null; } - /// - /// Finishes the stream. This will write the central directory at the - /// end of the zip file and flush the stream. - /// - /// The that can be used to cancel the operation. - /// - /// This is automatically called when the stream is closed. - /// - /// - /// An I/O error occurs. - /// - /// - /// Comment exceeds the maximum length
- /// Entry name exceeds the maximum length - ///
- public override async Task FinishAsync(CancellationToken cancellationToken) + private int WriteEndEntry(Stream stream, ZipEntry entry) { - if (entries == null) + stream.WriteLEInt(ZipConstants.CentralHeaderSignature); + stream.WriteLEShort((entry.HostSystem << 8) | entry.VersionMadeBy); + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((short)entry.CompressionMethodForHeader); + stream.WriteLEInt((int)entry.DosTime); + stream.WriteLEInt((int)entry.Crc); + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= uint.MaxValue)) { - return; + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); } - if (curEntry != null) + if (entry.IsZip64Forced() || + (entry.Size >= uint.MaxValue)) + { + stream.WriteLEInt(-1); + } + else { - await CloseEntryAsync(cancellationToken); + stream.WriteLEInt((int)entry.Size); } - long numEntries = entries.Count; - long sizeEntries = 0; + byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); - foreach (ZipEntry entry in entries) + if (name.Length > 0xffff) { - await WriteLeIntAsync(ZipConstants.CentralHeaderSignature, cancellationToken); - await WriteLeShortAsync((entry.HostSystem << 8) | entry.VersionMadeBy, cancellationToken); - await WriteLeShortAsync(entry.Version, cancellationToken); - await WriteLeShortAsync(entry.Flags, cancellationToken); - await WriteLeShortAsync((short)entry.CompressionMethodForHeader, cancellationToken); - await WriteLeIntAsync((int)entry.DosTime, cancellationToken); - await WriteLeIntAsync((int)entry.Crc, cancellationToken); + throw new ZipException("Name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + if (entry.CentralHeaderRequiresZip64) + { + ed.StartNewEntry(); if (entry.IsZip64Forced() || - (entry.CompressedSize >= uint.MaxValue)) - { - await WriteLeIntAsync(-1, cancellationToken); - } - else + (entry.Size >= 0xffffffff)) { - await WriteLeIntAsync((int)entry.CompressedSize, cancellationToken); + ed.AddLeLong(entry.Size); } if (entry.IsZip64Forced() || - (entry.Size >= uint.MaxValue)) - { - await WriteLeIntAsync(-1, cancellationToken); - } - else + (entry.CompressedSize >= 0xffffffff)) { - await WriteLeIntAsync((int)entry.Size, cancellationToken); + ed.AddLeLong(entry.CompressedSize); } - byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); - - if (name.Length > 0xffff) + if (entry.Offset >= 0xffffffff) { - throw new ZipException("Name too long."); + ed.AddLeLong(entry.Offset); } - var ed = new ZipExtraData(entry.ExtraData); + ed.AddNewEntry(1); + } + else + { + ed.Delete(1); + } - if (entry.CentralHeaderRequiresZip64) - { - ed.StartNewEntry(); - if (entry.IsZip64Forced() || - (entry.Size >= 0xffffffff)) - { - ed.AddLeLong(entry.Size); - } + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] extra = ed.GetEntryData(); - if (entry.IsZip64Forced() || - (entry.CompressedSize >= 0xffffffff)) - { - ed.AddLeLong(entry.CompressedSize); - } + byte[] entryComment = + (entry.Comment != null) ? + ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : + new byte[0]; - if (entry.Offset >= 0xffffffff) - { - ed.AddLeLong(entry.Offset); - } + if (entryComment.Length > 0xffff) + { + throw new ZipException("Comment too long."); + } - ed.AddNewEntry(1); + stream.WriteLEShort(name.Length); + stream.WriteLEShort(extra.Length); + stream.WriteLEShort(entryComment.Length); + stream.WriteLEShort(0); // disk number + stream.WriteLEShort(0); // internal file attributes + // external file attributes + + if (entry.ExternalFileAttributes != -1) + { + stream.WriteLEInt(entry.ExternalFileAttributes); + } + else + { + if (entry.IsDirectory) + { // mark entry as directory (from nikolam.AT.perfectinfo.com) + stream.WriteLEInt(16); } else { - ed.Delete(1); + stream.WriteLEInt(0); } + } - if (entry.AESKeySize > 0) - { - AddExtraDataAES(entry, ed); - } - byte[] extra = ed.GetEntryData(); + if (entry.Offset >= uint.MaxValue) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.Offset); + } - byte[] entryComment = - (entry.Comment != null) ? - ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : - new byte[0]; + if (name.Length > 0) + { + stream.Write(name, 0, name.Length); + } - if (entryComment.Length > 0xffff) - { - throw new ZipException("Comment too long."); - } + if (extra.Length > 0) + { + stream.Write(extra, 0, extra.Length); + } - await WriteLeShortAsync(name.Length, cancellationToken); - await WriteLeShortAsync(extra.Length, cancellationToken); - await WriteLeShortAsync(entryComment.Length, cancellationToken); - await WriteLeShortAsync(0, cancellationToken); // disk number - await WriteLeShortAsync(0, cancellationToken); // internal file attributes - // external file attributes + if (entryComment.Length > 0) + { + stream.Write(entryComment, 0, entryComment.Length); + } - if (entry.ExternalFileAttributes != -1) - { - await WriteLeIntAsync(entry.ExternalFileAttributes, cancellationToken); - } - else - { - if (entry.IsDirectory) - { // mark entry as directory (from nikolam.AT.perfectinfo.com) - await WriteLeIntAsync(16, cancellationToken); - } - else - { - await WriteLeIntAsync(0, cancellationToken); - } - } + return ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + } - if (entry.Offset >= uint.MaxValue) - { - await WriteLeIntAsync(-1, cancellationToken); - } - else + /// > + public override async Task FinishAsync(CancellationToken cancellationToken) + { + using (var ms = new MemoryStream()) + { + if (entries == null) { - await WriteLeIntAsync((int)entry.Offset, cancellationToken); + return; } - if (name.Length > 0) + if (curEntry != null) { - await baseOutputStream_.WriteAsync(name, 0, name.Length, cancellationToken); + await CloseEntryAsync(cancellationToken); } - if (extra.Length > 0) - { - await baseOutputStream_.WriteAsync(extra, 0, extra.Length, cancellationToken); - } + long numEntries = entries.Count; + long sizeEntries = 0; - if (entryComment.Length > 0) + foreach (var entry in entries) { - await baseOutputStream_.WriteAsync(entryComment, 0, entryComment.Length, cancellationToken); + sizeEntries += WriteEndEntry(ms, entry); + await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); + ms.SetLength(0); } - sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; - } + ZipFormat.WriteEndOfCentralDirectory(ms, numEntries, sizeEntries, offset, zipComment); + await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); -#if NETSTANDARD2_1 - await -#endif - using (ZipHelperStream zhs = new ZipHelperStream(baseOutputStream_)) - { - await zhs.WriteEndOfCentralDirectoryAsync(numEntries, sizeEntries, offset, zipComment, cancellationToken); + entries = null; } - - entries = null; } /// From bbb95170e8361a1dbd117f14cd24c94255f7e54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 7 Mar 2021 09:51:31 +0100 Subject: [PATCH 3/7] Fix tests --- .../Streams/DeflaterOutputStream.cs | 14 +- src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs | 254 ++++++++++- .../Zip/ZipOutputStream.cs | 412 +++--------------- .../TestSupport/SevenZip.cs | 9 +- .../Zip/ZipFileHandling.cs | 3 +- 5 files changed, 299 insertions(+), 393 deletions(-) diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs index f39442285..abe335ab2 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -262,10 +262,9 @@ protected void InitializePassword(string password) /// /// Initializes encryption keys based on given password. /// - protected void InitializeAESPassword(ZipEntry entry, string rawPassword, - out byte[] salt, out byte[] pwdVerifier) + protected byte[] InitializeAESPassword(ZipEntry entry, string rawPassword) { - salt = new byte[entry.AESSaltLen]; + var salt = new byte[entry.AESSaltLen]; // Salt needs to be cryptographically random, and unique per file if (_aesRnd == null) _aesRnd = RandomNumberGenerator.Create(); @@ -273,7 +272,14 @@ protected void InitializeAESPassword(ZipEntry entry, string rawPassword, int blockSize = entry.AESKeySize / 8; // bits to bytes cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); - pwdVerifier = ((ZipAESTransform)cryptoTransform_).PwdVerifier; + + var headBytes = new byte[salt.Length + 2]; + + Array.Copy(salt, headBytes, salt.Length); + Array.Copy(((ZipAESTransform)cryptoTransform_).PwdVerifier, 0, + headBytes, headBytes.Length - 2, 2); + + return headBytes; } #endregion Encryption diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs index 224a87eeb..3d34f22a4 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs @@ -33,7 +33,7 @@ public long Crc } } - internal class EntryPatchData + internal struct EntryPatchData { public long SizePatchOffset { get; set; } @@ -47,16 +47,15 @@ internal static class ZipFormat { // Write the local file header // TODO: ZipFormat.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage - private static void WriteLocalHeader(Stream stream, ZipEntry entry, EntryPatchData patchData, - bool headerInfoAvailable, bool patchEntryHeader, int offset) + internal static int WriteLocalHeader(Stream stream, ZipEntry entry, out EntryPatchData patchData, + bool headerInfoAvailable, bool patchEntryHeader, long streamOffset) { - CompressionMethod method = entry.CompressionMethod; - - stream.WriteLEInt(ZipConstants.LocalHeaderSignature); + patchData = new EntryPatchData(); + stream.WriteLEInt(ZipConstants.LocalHeaderSignature); stream.WriteLEShort(entry.Version); stream.WriteLEShort(entry.Flags); - stream.WriteLEShort((byte)method); + stream.WriteLEShort((byte)entry.CompressionMethodForHeader); stream.WriteLEInt((int)entry.DosTime); if (headerInfoAvailable) @@ -69,22 +68,19 @@ private static void WriteLocalHeader(Stream stream, ZipEntry entry, EntryPatchDa } else { - stream.WriteLEInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); + stream.WriteLEInt((int)entry.CompressedSize + entry.EncryptionOverheadSize); stream.WriteLEInt((int)entry.Size); } } else { - if (patchData != null) - { - patchData.CrcPatchOffset = offset + stream.Position; - } + if (patchEntryHeader) + patchData.CrcPatchOffset = streamOffset + stream.Position; + stream.WriteLEInt(0); // Crc - if (patchData != null) - { - patchData.SizePatchOffset = offset + stream.Position; - } + if (patchEntryHeader) + patchData.SizePatchOffset = streamOffset + stream.Position; // For local header both sizes appear in Zip64 Extended Information if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) @@ -108,13 +104,13 @@ private static void WriteLocalHeader(Stream stream, ZipEntry entry, EntryPatchDa var ed = new ZipExtraData(entry.ExtraData); - if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader)) + if (entry.LocalHeaderRequiresZip64) { ed.StartNewEntry(); if (headerInfoAvailable) { ed.AddLeLong(entry.Size); - ed.AddLeLong(entry.CompressedSize); + ed.AddLeLong(entry.CompressedSize + entry.EncryptionOverheadSize); } else { @@ -128,16 +124,17 @@ private static void WriteLocalHeader(Stream stream, ZipEntry entry, EntryPatchDa throw new ZipException("Internal error cant find extra data"); } - if (patchData != null) - { - patchData.SizePatchOffset = ed.CurrentReadIndex; - } + patchData.SizePatchOffset = ed.CurrentReadIndex; } else { ed.Delete(1); } + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } byte[] extra = ed.GetEntryData(); stream.WriteLEShort(name.Length); @@ -148,15 +145,17 @@ private static void WriteLocalHeader(Stream stream, ZipEntry entry, EntryPatchDa stream.Write(name, 0, name.Length); } - if (entry.LocalHeaderRequiresZip64 && patchEntryHeader && patchData != null) + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) { - patchData.SizePatchOffset += offset + stream.Position; + patchData.SizePatchOffset += streamOffset + stream.Position; } if (extra.Length > 0) { stream.Write(extra, 0, extra.Length); } + + return ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; } /// @@ -395,5 +394,212 @@ internal static void ReadDataDescriptor(Stream stream, bool zip64, DescriptorDat data.Size = stream.ReadLEInt(); } } + + internal static int WriteEndEntry(Stream stream, ZipEntry entry) + { + stream.WriteLEInt(ZipConstants.CentralHeaderSignature); + stream.WriteLEShort((entry.HostSystem << 8) | entry.VersionMadeBy); + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((short)entry.CompressionMethodForHeader); + stream.WriteLEInt((int)entry.DosTime); + stream.WriteLEInt((int)entry.Crc); + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= uint.MaxValue)) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + } + + if (entry.IsZip64Forced() || + (entry.Size >= uint.MaxValue)) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.Size); + } + + byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xffff) + { + throw new ZipException("Name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.CentralHeaderRequiresZip64) + { + ed.StartNewEntry(); + if (entry.IsZip64Forced() || + (entry.Size >= 0xffffffff)) + { + ed.AddLeLong(entry.Size); + } + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= 0xffffffff)) + { + ed.AddLeLong(entry.CompressedSize); + } + + if (entry.Offset >= 0xffffffff) + { + ed.AddLeLong(entry.Offset); + } + + ed.AddNewEntry(1); + } + else + { + ed.Delete(1); + } + + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] extra = ed.GetEntryData(); + + byte[] entryComment = + (entry.Comment != null) ? + ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : + new byte[0]; + + if (entryComment.Length > 0xffff) + { + throw new ZipException("Comment too long."); + } + + stream.WriteLEShort(name.Length); + stream.WriteLEShort(extra.Length); + stream.WriteLEShort(entryComment.Length); + stream.WriteLEShort(0); // disk number + stream.WriteLEShort(0); // internal file attributes + // external file attributes + + if (entry.ExternalFileAttributes != -1) + { + stream.WriteLEInt(entry.ExternalFileAttributes); + } + else + { + if (entry.IsDirectory) + { // mark entry as directory (from nikolam.AT.perfectinfo.com) + stream.WriteLEInt(16); + } + else + { + stream.WriteLEInt(0); + } + } + + if (entry.Offset >= uint.MaxValue) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.Offset); + } + + if (name.Length > 0) + { + stream.Write(name, 0, name.Length); + } + + if (extra.Length > 0) + { + stream.Write(extra, 0, extra.Length); + } + + if (entryComment.Length > 0) + { + stream.Write(entryComment, 0, entryComment.Length); + } + + return ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + } + + internal static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) + { + // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. + const int VENDOR_VERSION = 2; + // Vendor ID is the two ASCII characters "AE". + const int VENDOR_ID = 0x4541; //not 6965; + extraData.StartNewEntry(); + // Pack AES extra data field see http://www.winzip.com/aes_info.htm + //extraData.AddLeShort(7); // Data size (currently 7) + extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2 + extraData.AddLeShort(VENDOR_ID); // "AE" + extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256 + extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file + extraData.AddNewEntry(0x9901); + } + + internal static async Task PatchLocalHeaderAsync(Stream stream, ZipEntry entry, + EntryPatchData patchData, CancellationToken ct) + { + var initialPos = stream.Position; + + // Update CRC + stream.Seek(patchData.CrcPatchOffset, SeekOrigin.Begin); + await stream.WriteLEIntAsync((int)entry.Crc, ct); + + // Update Sizes + if (entry.LocalHeaderRequiresZip64) + { + if (patchData.SizePatchOffset == -1) + { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + + stream.Seek(patchData.SizePatchOffset, SeekOrigin.Begin); + + await stream.WriteLELongAsync(entry.Size, ct); + await stream.WriteLELongAsync(entry.CompressedSize, ct); + } + else + { + await stream.WriteLEIntAsync((int)entry.CompressedSize, ct); + await stream.WriteLEIntAsync((int)entry.Size, ct); + } + + stream.Seek(initialPos, SeekOrigin.Begin); + } + + internal static void PatchLocalHeaderSync(Stream stream, ZipEntry entry, + EntryPatchData patchData) + { + var initialPos = stream.Position; + stream.Seek(patchData.CrcPatchOffset, SeekOrigin.Begin); + stream.WriteLEInt((int)entry.Crc); + + if (entry.LocalHeaderRequiresZip64) + { + if (patchData.SizePatchOffset == -1) + { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + + stream.Seek(patchData.SizePatchOffset, SeekOrigin.Begin); + + stream.WriteLELong(entry.Size); + stream.WriteLELong(entry.CompressedSize); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + stream.WriteLEInt((int)entry.Size); + } + + stream.Seek(initialPos, SeekOrigin.Begin); + } } } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index 33100e685..49318f7cd 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -159,17 +159,10 @@ public UseZip64 UseZip64 // Apply any configured transforms/cleaning to the name of the supplied entry. private void TransformEntryName(ZipEntry entry) { - if (this.NameTransform != null) - { - if (entry.IsDirectory) - { - entry.Name = this.NameTransform.TransformDirectory(entry.Name); - } - else - { - entry.Name = this.NameTransform.TransformFile(entry.Name); - } - } + if (NameTransform == null) return; + entry.Name = entry.IsDirectory + ? NameTransform.TransformDirectory(entry.Name) + : NameTransform.TransformFile(entry.Name); } /// @@ -310,128 +303,21 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) entry.CompressionMethod = (CompressionMethod)method; curMethod = method; - sizePatchPos = -1; if ((useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic))) { entry.ForceZip64(); } - // Write the local file header - stream.WriteLEInt(ZipConstants.LocalHeaderSignature); - - stream.WriteLEShort(entry.Version); - stream.WriteLEShort(entry.Flags); - stream.WriteLEShort((byte)entry.CompressionMethodForHeader); - stream.WriteLEInt((int)entry.DosTime); - - // TODO: Refactor header writing. Its done in several places. - if (headerInfoAvailable) - { - stream.WriteLEInt((int)entry.Crc); - if (entry.LocalHeaderRequiresZip64) - { - stream.WriteLEInt(-1); - stream.WriteLEInt(-1); - } - else - { - stream.WriteLEInt((int)entry.CompressedSize + entry.EncryptionOverheadSize); - stream.WriteLEInt((int)entry.Size); - } - } - else - { - if (patchEntryHeader) - { - crcPatchPos = stream.Position + streamOffset; - } - stream.WriteLEInt(0); // Crc - - if (patchEntryHeader) - { - sizePatchPos = stream.Position + streamOffset; - } - - // For local header both sizes appear in Zip64 Extended Information - if (entry.LocalHeaderRequiresZip64 || patchEntryHeader) - { - stream.WriteLEInt(-1); - stream.WriteLEInt(-1); - } - else - { - stream.WriteLEInt(0); // Compressed size - stream.WriteLEInt(0); // Uncompressed size - } - } - - // Apply any required transforms to the entry name, and then convert to byte array format. + // Apply any required transforms to the entry name TransformEntryName(entry); - byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); - - if (name.Length > 0xFFFF) - { - throw new ZipException("Entry name too long."); - } - - var ed = new ZipExtraData(entry.ExtraData); - - if (entry.LocalHeaderRequiresZip64) - { - ed.StartNewEntry(); - if (headerInfoAvailable) - { - ed.AddLeLong(entry.Size); - ed.AddLeLong(entry.CompressedSize + entry.EncryptionOverheadSize); - } - else - { - ed.AddLeLong(-1); - ed.AddLeLong(-1); - } - ed.AddNewEntry(1); - - if (!ed.Find(1)) - { - throw new ZipException("Internal error cant find extra data"); - } - if (patchEntryHeader) - { - sizePatchPos = ed.CurrentReadIndex; - } - } - else - { - ed.Delete(1); - } - - if (entry.AESKeySize > 0) - { - AddExtraDataAES(entry, ed); - } - byte[] extra = ed.GetEntryData(); - - stream.WriteLEShort(name.Length); - stream.WriteLEShort(extra.Length); - - if (name.Length > 0) - { - baseOutputStream_.Write(name, 0, name.Length); - } + // Write the local file header + offset += ZipFormat.WriteLocalHeader(stream, entry, out var entryPatchData, + headerInfoAvailable, patchEntryHeader, streamOffset); - if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) - { - sizePatchPos += stream.Position + streamOffset; - } + patchData = entryPatchData; - if (extra.Length > 0) - { - baseOutputStream_.Write(extra, 0, extra.Length); - } - - offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; // Fix offsetOfCentraldir for AES if (entry.AESKeySize > 0) offset += entry.AESOverheadSize; @@ -446,23 +332,17 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) } size = 0; - if (entry.IsCrypted) + if (!entry.IsCrypted) + return; + + if (entry.AESKeySize > 0) { - if (entry.AESKeySize > 0) - { - WriteAESHeader(entry); - } - else - { - if (entry.Crc < 0) - { // so testing Zip will says its ok - WriteEncryptionHeader(entry.DosTime << 16); - } - else - { - WriteEncryptionHeader(entry.Crc); - } - } + WriteAESHeader(entry); + } + else + { + // Use entry time if CRC is missing + WriteEncryptionHeader(entry.Crc < 0 ? entry.DosTime << 16 : entry.Crc); } } @@ -517,17 +397,13 @@ public async Task PutNextEntryAsync(ZipEntry entry, CancellationToken cancellati /// public void CloseEntry() { - CloseEntry(baseOutputStream_); + WriteEntryFooter(baseOutputStream_); // Patch the header if possible if (patchEntryHeader) { patchEntryHeader = false; - - long curPos = baseOutputStream_.Position; - baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); - PatchLocalHeader(baseOutputStream_); - baseOutputStream_.Seek(curPos, SeekOrigin.Begin); + ZipFormat.PatchLocalHeaderSync(baseOutputStream_, curEntry, patchData); } entries.Add(curEntry); @@ -535,34 +411,26 @@ public void CloseEntry() } /// - public async Task CloseEntryAsync(CancellationToken cancellationToken) + public async Task CloseEntryAsync(CancellationToken ct) { using (var ms = new MemoryStream()) { - CloseEntry(ms); - await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); - - // Patch the header if possible - if (patchEntryHeader) - { - ms.Position = 0; - ms.SetLength(0); - PatchLocalHeader(ms); - - patchEntryHeader = false; + WriteEntryFooter(ms); + await ms.CopyToAsync(baseOutputStream_, 81920, ct); + } - long curPos = baseOutputStream_.Position; - baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); - await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); - baseOutputStream_.Seek(curPos, SeekOrigin.Begin); - } + // Patch the header if possible + if (patchEntryHeader) + { + patchEntryHeader = false; + await ZipFormat.PatchLocalHeaderAsync(baseOutputStream_, curEntry, patchData, ct); } entries.Add(curEntry); curEntry = null; } - internal void CloseEntry(Stream stream) + internal void WriteEntryFooter(Stream stream) { if (curEntry == null) { @@ -655,29 +523,7 @@ internal void CloseEntry(Stream stream) } } } - - private void PatchLocalHeader(Stream stream) - { - stream.WriteLEInt((int)curEntry.Crc); - - if (curEntry.LocalHeaderRequiresZip64) - { - if (sizePatchPos == -1) - { - throw new ZipException("Entry requires zip64 but this has been turned off"); - } - - baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); - stream.WriteLELong(curEntry.Size); - stream.WriteLELong(curEntry.CompressedSize); - } - else - { - stream.WriteLEInt((int)curEntry.CompressedSize); - stream.WriteLEInt((int)curEntry.Size); - } - } - + private byte[] CreateEncryptionHeader(long crcValue) { offset += ZipConstants.CryptoHeaderSize; @@ -709,50 +555,27 @@ private async Task WriteEncryptionHeaderAsync(long crcValue, CancellationToken c await baseOutputStream_.WriteAsync(cryptBuffer, 0, cryptBuffer.Length, cancellationToken); } - private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) - { - // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. - const int VENDOR_VERSION = 2; - // Vendor ID is the two ASCII characters "AE". - const int VENDOR_ID = 0x4541; //not 6965; - extraData.StartNewEntry(); - // Pack AES extra data field see http://www.winzip.com/aes_info.htm - //extraData.AddLeShort(7); // Data size (currently 7) - extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2 - extraData.AddLeShort(VENDOR_ID); // "AE" - extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256 - extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file - extraData.AddNewEntry(0x9901); - } - - private byte[] CreateAESHeader(ZipEntry entry) - { - InitializeAESPassword(entry, Password, out var salt, out var pwdVerifier); - // File format for AES: - // Size (bytes) Content - // ------------ ------- - // Variable Salt value - // 2 Password verification value - // Variable Encrypted file data - // 10 Authentication code - // - // Value in the "compressed size" fields of the local file header and the central directory entry - // is the total size of all the items listed above. In other words, it is the total size of the - // salt value, password verification value, encrypted data, and authentication code. - Array.Resize(ref salt, salt.Length + 2); - Array.Copy(pwdVerifier, 0, salt, salt.Length - 2, 2); - return salt; - } + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. private void WriteAESHeader(ZipEntry entry) { - var header = CreateAESHeader(entry); + var header = InitializeAESPassword(entry, Password); baseOutputStream_.Write(header, 0, header.Length); } private async Task WriteAESHeaderAsync(ZipEntry entry, CancellationToken cancellationToken) { - var header = CreateAESHeader(entry); + var header = InitializeAESPassword(entry, Password); await baseOutputStream_.WriteAsync(header, 0, header.Length, cancellationToken); } @@ -865,7 +688,7 @@ public override void Finish() foreach (var entry in entries) { - sizeEntries += WriteEndEntry(baseOutputStream_, entry); + sizeEntries += ZipFormat.WriteEndEntry(baseOutputStream_, entry); } ZipFormat.WriteEndOfCentralDirectory(baseOutputStream_, numEntries, sizeEntries, offset, zipComment); @@ -873,138 +696,6 @@ public override void Finish() entries = null; } - private int WriteEndEntry(Stream stream, ZipEntry entry) - { - stream.WriteLEInt(ZipConstants.CentralHeaderSignature); - stream.WriteLEShort((entry.HostSystem << 8) | entry.VersionMadeBy); - stream.WriteLEShort(entry.Version); - stream.WriteLEShort(entry.Flags); - stream.WriteLEShort((short)entry.CompressionMethodForHeader); - stream.WriteLEInt((int)entry.DosTime); - stream.WriteLEInt((int)entry.Crc); - - if (entry.IsZip64Forced() || - (entry.CompressedSize >= uint.MaxValue)) - { - stream.WriteLEInt(-1); - } - else - { - stream.WriteLEInt((int)entry.CompressedSize); - } - - if (entry.IsZip64Forced() || - (entry.Size >= uint.MaxValue)) - { - stream.WriteLEInt(-1); - } - else - { - stream.WriteLEInt((int)entry.Size); - } - - byte[] name = ZipStrings.ConvertToArray(entry.Flags, entry.Name); - - if (name.Length > 0xffff) - { - throw new ZipException("Name too long."); - } - - var ed = new ZipExtraData(entry.ExtraData); - - if (entry.CentralHeaderRequiresZip64) - { - ed.StartNewEntry(); - if (entry.IsZip64Forced() || - (entry.Size >= 0xffffffff)) - { - ed.AddLeLong(entry.Size); - } - - if (entry.IsZip64Forced() || - (entry.CompressedSize >= 0xffffffff)) - { - ed.AddLeLong(entry.CompressedSize); - } - - if (entry.Offset >= 0xffffffff) - { - ed.AddLeLong(entry.Offset); - } - - ed.AddNewEntry(1); - } - else - { - ed.Delete(1); - } - - if (entry.AESKeySize > 0) - { - AddExtraDataAES(entry, ed); - } - byte[] extra = ed.GetEntryData(); - - byte[] entryComment = - (entry.Comment != null) ? - ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : - new byte[0]; - - if (entryComment.Length > 0xffff) - { - throw new ZipException("Comment too long."); - } - - stream.WriteLEShort(name.Length); - stream.WriteLEShort(extra.Length); - stream.WriteLEShort(entryComment.Length); - stream.WriteLEShort(0); // disk number - stream.WriteLEShort(0); // internal file attributes - // external file attributes - - if (entry.ExternalFileAttributes != -1) - { - stream.WriteLEInt(entry.ExternalFileAttributes); - } - else - { - if (entry.IsDirectory) - { // mark entry as directory (from nikolam.AT.perfectinfo.com) - stream.WriteLEInt(16); - } - else - { - stream.WriteLEInt(0); - } - } - - if (entry.Offset >= uint.MaxValue) - { - stream.WriteLEInt(-1); - } - else - { - stream.WriteLEInt((int)entry.Offset); - } - - if (name.Length > 0) - { - stream.Write(name, 0, name.Length); - } - - if (extra.Length > 0) - { - stream.Write(extra, 0, extra.Length); - } - - if (entryComment.Length > 0) - { - stream.Write(entryComment, 0, entryComment.Length); - } - - return ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; - } - /// > public override async Task FinishAsync(CancellationToken cancellationToken) { @@ -1025,7 +716,7 @@ public override async Task FinishAsync(CancellationToken cancellationToken) foreach (var entry in entries) { - sizeEntries += WriteEndEntry(ms, entry); + sizeEntries += ZipFormat.WriteEndEntry(ms, entry); await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); ms.SetLength(0); } @@ -1095,14 +786,9 @@ public override void Flush() private bool patchEntryHeader; /// - /// Position to patch crc - /// - private long crcPatchPos = -1; - - /// - /// Position to patch size. + /// The values to patch in the entry local header /// - private long sizePatchPos = -1; + private EntryPatchData patchData; // Default is dynamic which is not backwards compatible and can cause problems // with XP's built in compression which cant read Zip64 archives. diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs index c312ec401..9059c4ed2 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs @@ -77,12 +77,19 @@ internal static void VerifyZipWith7Zip(Stream zipStream, string password) zipStream.CopyTo(fs); } - var p = Process.Start(path7z, $"t -p{password} \"{fileName}\""); + var p = Process.Start(new ProcessStartInfo() + { + FileName = path7z, + Arguments = $"t -p{password} \"{fileName}\"", + RedirectStandardError = true + }); if (!p.WaitForExit(2000)) { Assert.Warn("Timed out verifying zip file!"); } + Console.Write(p.StandardError.ReadToEnd()); + Assert.AreEqual(0, p.ExitCode, "Archive verification failed"); } finally diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs index c92dae280..6de77ae4e 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs @@ -887,7 +887,8 @@ private void TestEncryptedDirectoryEntry(MemoryStream s, int aesKeySize) var ms2 = new MemoryStream(s.ToArray()); using (ZipFile zf = new ZipFile(ms2)) { - Assert.IsTrue(zf.TestArchive(true)); + Assert.IsTrue(zf.TestArchive(true, TestStrategy.FindAllErrors, + (status, message) => Console.WriteLine(message))); } } From 870fcbe811596bb4378b782c476683d4a0caafa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Mon, 10 May 2021 23:56:33 +0200 Subject: [PATCH 4/7] Fix error output in tests from empty logging --- test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs index 7305d95b9..2d9006707 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs @@ -917,7 +917,9 @@ private void TestEncryptedDirectoryEntry(MemoryStream s, int aesKeySize) using (ZipFile zf = new ZipFile(ms2)) { Assert.IsTrue(zf.TestArchive(true, TestStrategy.FindAllErrors, - (status, message) => Console.WriteLine(message))); + (status, message) => { + if (!string.IsNullOrWhiteSpace(message)) TestContext.Out.WriteLine(message); + })); } } From 89f589de70d220bd8a2ef861f6cee72ad1116fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Thu, 12 Aug 2021 16:18:51 +0200 Subject: [PATCH 5/7] test: add byteorder tests --- src/ICSharpCode.SharpZipLib/AssemblyInfo.cs | 3 + .../Core/ByteOrderUtils.cs | 31 ++-- .../Core/ByteOrderUtilsTests.cs | 137 ++++++++++++++++++ .../ICSharpCode.SharpZipLib.Tests.csproj | 9 ++ 4 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 src/ICSharpCode.SharpZipLib/AssemblyInfo.cs create mode 100644 test/ICSharpCode.SharpZipLib.Tests/Core/ByteOrderUtilsTests.cs diff --git a/src/ICSharpCode.SharpZipLib/AssemblyInfo.cs b/src/ICSharpCode.SharpZipLib/AssemblyInfo.cs new file mode 100644 index 000000000..8f8e62016 --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ICSharpCode.SharpZipLib.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b9a14ea8fc9d7599e0e82a1292a23103f0210e2f928a0f466963af23fffadba59dcc8c9e26ecd114d7c0b4179e4bc93b1656b7ee2d4a67dd7c1992653e0d9cc534f7914b6f583b022e0a7aa8a430f407932f9a6806f0fc64d61e78d5ae01aa8f8233196719d44da2c50a2d1cfa3f7abb7487b3567a4f0456aa6667154c6749b1")] diff --git a/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs b/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs index 0edb0964c..a2e30da7f 100644 --- a/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs +++ b/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs @@ -3,46 +3,49 @@ using System.Threading.Tasks; using CT = System.Threading.CancellationToken; +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming + namespace ICSharpCode.SharpZipLib.Core { internal static class ByteOrderStreamExtensions { - static byte[] SwappedBytes(ushort value) => new[] {(byte)value, (byte)(value >> 8)}; - static byte[] SwappedBytes(short value) => new[] {(byte)value, (byte)(value >> 8)}; - static byte[] SwappedBytes(uint value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; - static byte[] SwappedBytes(int value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; + internal static byte[] SwappedBytes(ushort value) => new[] {(byte)value, (byte)(value >> 8)}; + internal static byte[] SwappedBytes(short value) => new[] {(byte)value, (byte)(value >> 8)}; + internal static byte[] SwappedBytes(uint value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; + internal static byte[] SwappedBytes(int value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; - static byte[] SwappedBytes(long value) => new[] { + internal static byte[] SwappedBytes(long value) => new[] { (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), - (byte)(value << 32), (byte)(value << 40), (byte)(value << 48), (byte)(value << 56) + (byte)(value >> 32), (byte)(value >> 40), (byte)(value >> 48), (byte)(value >> 56) }; - static byte[] SwappedBytes(ulong value) => new[] { + internal static byte[] SwappedBytes(ulong value) => new[] { (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), - (byte)(value << 32), (byte)(value << 40), (byte)(value << 48), (byte)(value << 56) + (byte)(value >> 32), (byte)(value >> 40), (byte)(value >> 48), (byte)(value >> 56) }; [MethodImpl(MethodImplOptions.AggressiveInlining)] - static long SwappedS64(byte[] bytes) => ( + internal static long SwappedS64(byte[] bytes) => ( (long)bytes[0] << 0 | (long)bytes[1] << 8 | (long)bytes[2] << 16 | (long)bytes[3] << 24 | (long)bytes[4] << 32 | (long)bytes[5] << 40 | (long)bytes[6] << 48 | (long)bytes[7] << 56); [MethodImpl(MethodImplOptions.AggressiveInlining)] - static ulong SwappedU64(byte[] bytes) => ( + internal static ulong SwappedU64(byte[] bytes) => ( (ulong)bytes[0] << 0 | (ulong)bytes[1] << 8 | (ulong)bytes[2] << 16 | (ulong)bytes[3] << 24 | (ulong)bytes[4] << 32 | (ulong)bytes[5] << 40 | (ulong)bytes[6] << 48 | (ulong)bytes[7] << 56); [MethodImpl(MethodImplOptions.AggressiveInlining)] - static int SwappedS32(byte[] bytes) => bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24; + internal static int SwappedS32(byte[] bytes) => bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24; [MethodImpl(MethodImplOptions.AggressiveInlining)] - static uint SwappedU32(byte[] bytes) => (uint) SwappedS32(bytes); + internal static uint SwappedU32(byte[] bytes) => (uint) SwappedS32(bytes); [MethodImpl(MethodImplOptions.AggressiveInlining)] - static short SwappedS16(byte[] bytes) => (short)(bytes[0] | bytes[1] << 8); + internal static short SwappedS16(byte[] bytes) => (short)(bytes[0] | bytes[1] << 8); [MethodImpl(MethodImplOptions.AggressiveInlining)] - static ushort SwappedU16(byte[] bytes) => (ushort) SwappedS16(bytes); + internal static ushort SwappedU16(byte[] bytes) => (ushort) SwappedS16(bytes); internal static byte[] ReadBytes(this Stream stream, int count) { diff --git a/test/ICSharpCode.SharpZipLib.Tests/Core/ByteOrderUtilsTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Core/ByteOrderUtilsTests.cs new file mode 100644 index 000000000..1a5d271ff --- /dev/null +++ b/test/ICSharpCode.SharpZipLib.Tests/Core/ByteOrderUtilsTests.cs @@ -0,0 +1,137 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using BO = ICSharpCode.SharpZipLib.Core.ByteOrderStreamExtensions; +using ICSharpCode.SharpZipLib.Core; + +// ReSharper disable InconsistentNaming + +namespace ICSharpCode.SharpZipLib.Tests.Core +{ + [TestFixture] + [Category("Core")] + public class ByteOrderUtilsTests + { + private const short native16 = 0x1234; + private static readonly byte[] swapped16 = { 0x34, 0x12 }; + + private const int native32 = 0x12345678; + private static readonly byte[] swapped32 = { 0x78, 0x56, 0x34, 0x12 }; + + private const long native64 = 0x123456789abcdef0; + private static readonly byte[] swapped64 = { 0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12 }; + + [Test] + public void ToSwappedBytes() + { + Assert.AreEqual(swapped16, BO.SwappedBytes(native16)); + Assert.AreEqual(swapped16, BO.SwappedBytes((ushort)native16)); + + Assert.AreEqual(swapped32, BO.SwappedBytes(native32)); + Assert.AreEqual(swapped32, BO.SwappedBytes((uint)native32)); + + Assert.AreEqual(swapped64, BO.SwappedBytes(native64)); + Assert.AreEqual(swapped64, BO.SwappedBytes((ulong)native64)); + } + + [Test] + public void FromSwappedBytes() + { + Assert.AreEqual(native16, BO.SwappedS16(swapped16)); + Assert.AreEqual(native16, BO.SwappedU16(swapped16)); + + Assert.AreEqual(native32, BO.SwappedS32(swapped32)); + Assert.AreEqual(native32, BO.SwappedU32(swapped32)); + + Assert.AreEqual(native64, BO.SwappedS64(swapped64)); + Assert.AreEqual(native64, BO.SwappedU64(swapped64)); + } + + [Test] + public void ReadLESigned16() + => TestReadLE(native16, 2, BO.ReadLEShort); + + [Test] + public void ReadLESigned32() + => TestReadLE(native32,4, BO.ReadLEInt); + + [Test] + public void ReadLESigned64() + => TestReadLE(native64,8, BO.ReadLELong); + + [Test] + public void WriteLESigned16() + => TestWriteLE(swapped16, s => s.WriteLEShort(native16)); + + [Test] + public void WriteLESigned32() + => TestWriteLE(swapped32, s => s.WriteLEInt(native32)); + + [Test] + public void WriteLESigned64() + => TestWriteLE(swapped64, s => s.WriteLELong(native64)); + + [Test] + public void WriteLEUnsigned16() + => TestWriteLE(swapped16, s => s.WriteLEUshort((ushort)native16)); + + [Test] + public void WriteLEUnsigned32() + => TestWriteLE(swapped32, s => s.WriteLEUint(native32)); + + [Test] + public void WriteLEUnsigned64() + => TestWriteLE(swapped64, s => s.WriteLEUlong(native64)); + + [Test] + public async Task WriteLEAsyncSigned16() + => await TestWriteLEAsync(swapped16, (int)native16, BO.WriteLEShortAsync); + + [Test] + public async Task WriteLEAsyncUnsigned16() + => await TestWriteLEAsync(swapped16, (ushort)native16, BO.WriteLEUshortAsync); + + [Test] + public async Task WriteLEAsyncSigned32() + => await TestWriteLEAsync(swapped32, native32, BO.WriteLEIntAsync); + [Test] + public async Task WriteLEAsyncUnsigned32() + => await TestWriteLEAsync(swapped32, (uint)native32, BO.WriteLEUintAsync); + + [Test] + public async Task WriteLEAsyncSigned64() + => await TestWriteLEAsync(swapped64, native64, BO.WriteLELongAsync); + [Test] + public async Task WriteLEAsyncUnsigned64() + => await TestWriteLEAsync(swapped64, (ulong)native64, BO.WriteLEUlongAsync); + + + private static void TestReadLE(T expected, int bytes, Func read) + { + using (var ms = new MemoryStream(swapped64, 8 - bytes, bytes)) + { + Assert.AreEqual(expected, read(ms)); + } + } + + private static void TestWriteLE(byte[] expected, Action write) + { + using (var ms = new MemoryStream()) + { + write(ms); + Assert.AreEqual(expected, ms.ToArray()); + } + } + + private static async Task TestWriteLEAsync(byte[] expected, T input, Func write) + { + using (var ms = new MemoryStream()) + { + await write(ms, input, CancellationToken.None); + Assert.AreEqual(expected, ms.ToArray()); + } + } + } +} diff --git a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj index fd6f61ae9..9f75d7e35 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj +++ b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj @@ -5,6 +5,9 @@ netcoreapp3.1;net46 + true + ..\..\assets\ICSharpCode.SharpZipLib.snk + true @@ -24,4 +27,10 @@ + + + ICSharpCode.SharpZipLib.snk + + + From ce6e38bbc0a9a26a377fa36017ac66ec89401a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Thu, 12 Aug 2021 21:12:17 +0200 Subject: [PATCH 6/7] add async tests and bugfixes/cleanup --- .../Core/StreamUtils.cs | 19 +++ .../Streams/DeflaterOutputStream.cs | 25 +-- src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs | 25 +-- .../Zip/ZipOutputStream.cs | 155 ++++++++---------- .../ICSharpCode.SharpZipLib.Tests.csproj | 1 + .../TestSupport/Utils.cs | 27 ++- .../TestSupport/ZipTesting.cs | 47 +++++- .../Zip/ZipStreamAsyncTests.cs | 70 ++++++++ 8 files changed, 235 insertions(+), 134 deletions(-) create mode 100644 test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs diff --git a/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs b/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs index a46ee9337..47de6e26e 100644 --- a/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs +++ b/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Core { @@ -272,5 +274,22 @@ public static void Copy(Stream source, Stream destination, progressHandler(sender, args); } } + + internal static async Task WriteProcToStreamAsync(this Stream targetStream, MemoryStream bufferStream, Action writeProc, CancellationToken ct) + { + bufferStream.SetLength(0); + writeProc(bufferStream); + bufferStream.Position = 0; + await bufferStream.CopyToAsync(targetStream, 81920, ct); + bufferStream.SetLength(0); + } + + internal static async Task WriteProcToStreamAsync(this Stream targetStream, Action writeProc, CancellationToken ct) + { + using (var ms = new MemoryStream()) + { + await WriteProcToStreamAsync(targetStream, ms, writeProc, ct); + } + } } } diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs index 0de0877df..fd4bb47af 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -107,10 +107,7 @@ public virtual void Finish() break; } - if (cryptoTransform_ != null) - { - EncryptBlock(buffer_, 0, len); - } + EncryptBlock(buffer_, 0, len); baseOutputStream_.Write(buffer_, 0, len); } @@ -136,11 +133,11 @@ public virtual void Finish() /// /// Finishes the stream by calling finish() on the deflater. /// - /// The that can be used to cancel the operation. + /// The that can be used to cancel the operation. /// /// Not all input is deflated /// - public virtual async Task FinishAsync(CancellationToken cancellationToken) + public virtual async Task FinishAsync(CancellationToken ct) { deflater_.Finish(); while (!deflater_.IsFinished) @@ -151,12 +148,9 @@ public virtual async Task FinishAsync(CancellationToken cancellationToken) break; } - if (cryptoTransform_ != null) - { - EncryptBlock(buffer_, 0, len); - } + EncryptBlock(buffer_, 0, len); - await baseOutputStream_.WriteAsync(buffer_, 0, len, cancellationToken); + await baseOutputStream_.WriteAsync(buffer_, 0, len, ct); } if (!deflater_.IsFinished) @@ -164,7 +158,7 @@ public virtual async Task FinishAsync(CancellationToken cancellationToken) throw new SharpZipBaseException("Can't deflate all input?"); } - await baseOutputStream_.FlushAsync(cancellationToken); + await baseOutputStream_.FlushAsync(ct); if (cryptoTransform_ != null) { @@ -223,6 +217,7 @@ public bool CanPatchEntries /// protected void EncryptBlock(byte[] buffer, int offset, int length) { + if(cryptoTransform_ is null) return; cryptoTransform_.TransformBlock(buffer, 0, length, buffer, 0); } @@ -250,10 +245,8 @@ private void Deflate(bool flushing) { break; } - if (cryptoTransform_ != null) - { - EncryptBlock(buffer_, 0, deflateCount); - } + + EncryptBlock(buffer_, 0, deflateCount); baseOutputStream_.Write(buffer_, 0, deflateCount); } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs index 6479e6dd2..75f6b72d7 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs @@ -194,11 +194,7 @@ internal static long LocateBlockWithSignature(Stream stream, int signature, long public static async Task WriteZip64EndOfCentralDirectoryAsync(Stream stream, long noOfEntries, long sizeEntries, long centralDirOffset, CancellationToken cancellationToken) { - using (var ms = new MemoryStream()) - { - WriteZip64EndOfCentralDirectory(ms, noOfEntries, sizeEntries, centralDirOffset); - await ms.CopyToAsync(stream, 81920, cancellationToken); - } + await stream.WriteProcToStreamAsync(s => WriteZip64EndOfCentralDirectory(s, noOfEntries, sizeEntries, centralDirOffset), cancellationToken); } /// @@ -238,16 +234,10 @@ internal static void WriteZip64EndOfCentralDirectory(Stream stream, long noOfEnt /// public static async Task WriteEndOfCentralDirectoryAsync(Stream stream, long noOfEntries, long sizeEntries, - long start, byte[] comment, CancellationToken cancellationToken) - { - using (var ms = new MemoryStream()) - { - WriteEndOfCentralDirectory(ms, noOfEntries, sizeEntries, start, comment); - await ms.CopyToAsync(stream, 81920, cancellationToken); - } - } - - + long start, byte[] comment, CancellationToken cancellationToken) + => await stream.WriteProcToStreamAsync(s + => WriteEndOfCentralDirectory(s, noOfEntries, sizeEntries, start, comment), cancellationToken); + /// /// Write the required records to end the central directory. /// @@ -558,9 +548,10 @@ internal static async Task PatchLocalHeaderAsync(Stream stream, ZipEntry entry, { throw new ZipException("Entry requires zip64 but this has been turned off"); } - + // Seek to the Zip64 Extra Data stream.Seek(patchData.SizePatchOffset, SeekOrigin.Begin); + // Note: The order of the size fields is reversed when compared to the local header! await stream.WriteLELongAsync(entry.Size, ct); await stream.WriteLELongAsync(entry.CompressedSize, ct); } @@ -587,8 +578,10 @@ internal static void PatchLocalHeaderSync(Stream stream, ZipEntry entry, throw new ZipException("Entry requires zip64 but this has been turned off"); } + // Seek to the Zip64 Extra Data stream.Seek(patchData.SizePatchOffset, SeekOrigin.Begin); + // Note: The order of the size fields is reversed when compared to the local header! stream.WriteLELong(entry.Size); stream.WriteLELong(entry.CompressedSize); } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index 3c80483ca..7aa3295fe 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -253,7 +253,30 @@ private void TransformEntryName(ZipEntry entry) /// The Compression method specified for the entry is unsupported. /// public void PutNextEntry(ZipEntry entry) - => PutNextEntry(baseOutputStream_, entry); + { + if (curEntry != null) + { + CloseEntry(); + } + + PutNextEntry(baseOutputStream_, entry); + + if (entry.IsCrypted) + { + WriteOutput(GetEntryEncryptionHeader(entry)); + } + } + + private void WriteOutput(byte[] bytes) + => baseOutputStream_.Write(bytes, 0, bytes.Length); + + private Task WriteOutputAsync(byte[] bytes) + => baseOutputStream_.WriteAsync(bytes, 0, bytes.Length); + + private byte[] GetEntryEncryptionHeader(ZipEntry entry) => + entry.AESKeySize > 0 + ? InitializeAESPassword(entry, Password) + : CreateZipCryptoHeader(entry.Crc < 0 ? entry.DosTime << 16 : entry.Crc); internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) { @@ -267,11 +290,6 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) throw new InvalidOperationException("ZipOutputStream was finished"); } - if (curEntry != null) - { - CloseEntry(); - } - if (entries.Count == int.MaxValue) { throw new ZipException("Too many entries for Zip file"); @@ -391,19 +409,7 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) deflater_.SetLevel(compressionLevel); } size = 0; - - if (!entry.IsCrypted) - return; - - if (entry.AESKeySize > 0) - { - WriteAESHeader(entry); - } - else - { - // Use entry time if CRC is missing - WriteEncryptionHeader(entry.Crc < 0 ? entry.DosTime << 16 : entry.Crc); - } + } /// @@ -416,7 +422,7 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) /// /// the entry. /// - /// The that can be used to cancel the operation. + /// The that can be used to cancel the operation. /// /// if entry passed is null. /// @@ -434,13 +440,16 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0) /// /// The Compression method specified for the entry is unsupported. /// - public async Task PutNextEntryAsync(ZipEntry entry, CancellationToken cancellationToken = default) + public async Task PutNextEntryAsync(ZipEntry entry, CancellationToken ct = default) { - using (var ms = new MemoryStream()) - { - PutNextEntry(ms, entry, baseOutputStream_.Position); - await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); - } + if (curEntry != null) await CloseEntryAsync(ct); + await baseOutputStream_.WriteProcToStreamAsync(s => + { + PutNextEntry(s, entry, baseOutputStream_.Position); + }, ct); + + if (!entry.IsCrypted) return; + await WriteOutputAsync(GetEntryEncryptionHeader(entry)); } /// @@ -473,11 +482,7 @@ public void CloseEntry() /// public async Task CloseEntryAsync(CancellationToken ct) { - using (var ms = new MemoryStream()) - { - WriteEntryFooter(ms); - await ms.CopyToAsync(baseOutputStream_, 81920, ct); - } + await baseOutputStream_.WriteProcToStreamAsync(WriteEntryFooter, ct); // Patch the header if possible if (patchEntryHeader) @@ -584,17 +589,20 @@ internal void WriteEntryFooter(Stream stream) } } - /// - /// Initializes encryption keys based on given . - /// - /// The password. - private void InitializePassword(string password) - { - var pkManaged = new PkzipClassicManaged(); - byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password)); - cryptoTransform_ = pkManaged.CreateEncryptor(key, null); - } + + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. + /// /// Initializes encryption keys based on given password. /// @@ -618,11 +626,11 @@ protected byte[] InitializeAESPassword(ZipEntry entry, string rawPassword) return headBytes; } - private byte[] CreateEncryptionHeader(long crcValue) + private byte[] CreateZipCryptoHeader(long crcValue) { offset += ZipConstants.CryptoHeaderSize; - InitializePassword(Password); + InitializeZipCryptoPassword(Password); byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; using (var rng = new RNGCryptoServiceProvider()) @@ -636,43 +644,18 @@ private byte[] CreateEncryptionHeader(long crcValue) return cryptBuffer; } - - private void WriteEncryptionHeader(long crcValue) - { - var cryptBuffer = CreateEncryptionHeader(crcValue); - baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length); - } - - private async Task WriteEncryptionHeaderAsync(long crcValue, CancellationToken cancellationToken) - { - var cryptBuffer = CreateEncryptionHeader(crcValue); - await baseOutputStream_.WriteAsync(cryptBuffer, 0, cryptBuffer.Length, cancellationToken); - } - - // File format for AES: - // Size (bytes) Content - // ------------ ------- - // Variable Salt value - // 2 Password verification value - // Variable Encrypted file data - // 10 Authentication code - // - // Value in the "compressed size" fields of the local file header and the central directory entry - // is the total size of all the items listed above. In other words, it is the total size of the - // salt value, password verification value, encrypted data, and authentication code. - - private void WriteAESHeader(ZipEntry entry) - { - var header = InitializeAESPassword(entry, Password); - baseOutputStream_.Write(header, 0, header.Length); - } - - private async Task WriteAESHeaderAsync(ZipEntry entry, CancellationToken cancellationToken) + + /// + /// Initializes encryption keys based on given . + /// + /// The password. + private void InitializeZipCryptoPassword(string password) { - var header = InitializeAESPassword(entry, Password); - await baseOutputStream_.WriteAsync(header, 0, header.Length, cancellationToken); + var pkManaged = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password)); + cryptoTransform_ = pkManaged.CreateEncryptor(key, null); } - + /// /// Writes the given buffer to the current entry. /// @@ -791,7 +774,7 @@ public override void Finish() } /// > - public override async Task FinishAsync(CancellationToken cancellationToken) + public override async Task FinishAsync(CancellationToken ct) { using (var ms = new MemoryStream()) { @@ -802,7 +785,7 @@ public override async Task FinishAsync(CancellationToken cancellationToken) if (curEntry != null) { - await CloseEntryAsync(cancellationToken); + await CloseEntryAsync(ct); } long numEntries = entries.Count; @@ -810,13 +793,15 @@ public override async Task FinishAsync(CancellationToken cancellationToken) foreach (var entry in entries) { - sizeEntries += ZipFormat.WriteEndEntry(ms, entry); - await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); - ms.SetLength(0); + await baseOutputStream_.WriteProcToStreamAsync(ms, s => + { + sizeEntries += ZipFormat.WriteEndEntry(s, entry); + }, ct); } - ZipFormat.WriteEndOfCentralDirectory(ms, numEntries, sizeEntries, offset, zipComment); - await ms.CopyToAsync(baseOutputStream_, 81920, cancellationToken); + await baseOutputStream_.WriteProcToStreamAsync(ms, s + => ZipFormat.WriteEndOfCentralDirectory(s, numEntries, sizeEntries, offset, zipComment), + ct); entries = null; } diff --git a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj index 9f75d7e35..4a46e84f2 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj +++ b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj @@ -8,6 +8,7 @@ true ..\..\assets\ICSharpCode.SharpZipLib.snk true + 8.0 diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs index 33d6e3e9b..a95d368fb 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Tests.TestSupport { @@ -48,19 +49,29 @@ public static void WriteDummyData(string fileName, int size = -1) } } - public static void WriteDummyData(Stream stream, int size = -1) + public static byte[] GetDummyData(int size = -1) { - var bytes = (size < 0) - ? Encoding.ASCII.GetBytes(DateTime.UtcNow.Ticks.ToString("x16")) - : new byte[size]; - - if(size > 0) + if (size < 0) { - random.NextBytes(bytes); + return Encoding.ASCII.GetBytes(DateTime.UtcNow.Ticks.ToString("x16")); } - + + var bytes = new byte[size]; + random.NextBytes(bytes); + return bytes; + } + + public static void WriteDummyData(Stream stream, int size = -1) + { + var bytes = GetDummyData(size); stream.Write(bytes, 0, bytes.Length); } + + public static async Task WriteDummyDataAsync(Stream stream, int size = -1) + { + var bytes = GetDummyData(size); + await stream.WriteAsync(bytes, 0, bytes.Length); + } public static TempFile GetDummyFile(int size = -1) { diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/ZipTesting.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/ZipTesting.cs index 688b91dc3..fcc4fe9b8 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/ZipTesting.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/ZipTesting.cs @@ -1,5 +1,7 @@ +using System; using ICSharpCode.SharpZipLib.Zip; using System.IO; +using NUnit.Framework; namespace ICSharpCode.SharpZipLib.Tests.TestSupport { @@ -8,30 +10,57 @@ namespace ICSharpCode.SharpZipLib.Tests.TestSupport /// internal static class ZipTesting { + public static void AssertValidZip(Stream stream, string password = null, bool usesAes = true) + { + Assert.That(TestArchive(stream, password), "Archive did not pass ZipFile.TestArchive"); + + if (!string.IsNullOrEmpty(password) && usesAes) + { + Assert.Ignore("ZipInputStream does not support AES"); + } + + stream.Seek(0, SeekOrigin.Begin); + + Assert.DoesNotThrow(() => + { + using var zis = new ZipInputStream(stream){Password = password}; + while (zis.GetNextEntry() != null) + { + new StreamReader(zis).ReadToEnd(); + } + }, "Archive could not be read by ZipInputStream"); + } + /// /// Tests the archive. /// /// The data. + /// The password. /// - public static bool TestArchive(byte[] data) + public static bool TestArchive(byte[] data, string password = null) { - return TestArchive(data, null); + using var ms = new MemoryStream(data); + return TestArchive(new MemoryStream(data), password); } /// /// Tests the archive. /// - /// The data. + /// The data. /// The password. /// true if archive tests ok; false otherwise. - public static bool TestArchive(byte[] data, string password) + public static bool TestArchive(Stream stream, string password = null) { - using (MemoryStream ms = new MemoryStream(data)) - using (ZipFile zipFile = new ZipFile(ms)) + using var zipFile = new ZipFile(stream) { - zipFile.Password = password; - return zipFile.TestArchive(true); - } + IsStreamOwner = false, + Password = password, + }; + + return zipFile.TestArchive(true, TestStrategy.FindAllErrors, (status, message) => + { + if (!string.IsNullOrWhiteSpace(message)) TestContext.Out.WriteLine(message); + }); } } } diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs new file mode 100644 index 000000000..0efcf3ac7 --- /dev/null +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs @@ -0,0 +1,70 @@ +using System.IO; +using System.Threading.Tasks; +using ICSharpCode.SharpZipLib.Tests.TestSupport; +using ICSharpCode.SharpZipLib.Zip; +using NUnit.Framework; + +namespace ICSharpCode.SharpZipLib.Tests.Zip +{ + [TestFixture] + public class ZipStreamAsyncTests + { +#if NETCOREAPP3_1_OR_GREATER + [Test] + [Category("Zip")] + public async Task WriteZipStreamAsync () + { + await using var ms = new MemoryStream(); + + await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false}) + { + await outStream.PutNextEntryAsync(new ZipEntry("FirstFile")); + await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.PutNextEntryAsync(new ZipEntry("SecondFile")); + await Utils.WriteDummyDataAsync(outStream, 12); + } + + ZipTesting.AssertValidZip(ms); + } + + [Test] + [Category("Zip")] + public async Task WriteZipStreamWithAesAsync() + { + await using var ms = new MemoryStream(); + var password = "f4ls3p0s1t1v3"; + + await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false, Password = password}) + { + await outStream.PutNextEntryAsync(new ZipEntry("FirstFile"){AESKeySize = 256}); + await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.PutNextEntryAsync(new ZipEntry("SecondFile"){AESKeySize = 256}); + await Utils.WriteDummyDataAsync(outStream, 12); + } + + ZipTesting.AssertValidZip(ms, password); + } + + [Test] + [Category("Zip")] + public async Task WriteZipStreamWithZipCryptoAsync() + { + await using var ms = new MemoryStream(); + var password = "f4ls3p0s1t1v3"; + + await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false, Password = password}) + { + await outStream.PutNextEntryAsync(new ZipEntry("FirstFile"){AESKeySize = 0}); + await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.PutNextEntryAsync(new ZipEntry("SecondFile"){AESKeySize = 0}); + await Utils.WriteDummyDataAsync(outStream, 12); + } + + ZipTesting.AssertValidZip(ms, password, false); + } +#endif + } +} From 3473cfac6bc44240e93143b1efd24c61f8d0faaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Thu, 12 Aug 2021 22:51:26 +0200 Subject: [PATCH 7/7] fix tests --- .../Base/InflaterDeflaterTests.cs | 3 +- .../Zip/ZipStreamAsyncTests.cs | 46 ++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/test/ICSharpCode.SharpZipLib.Tests/Base/InflaterDeflaterTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Base/InflaterDeflaterTests.cs index e6e3c4125..0b8a52b19 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Base/InflaterDeflaterTests.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Base/InflaterDeflaterTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Security; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Tests.Base @@ -123,7 +124,7 @@ private async Task DeflateAsync(byte[] data, int level, bool zlib) outStream.IsStreamOwner = false; await outStream.WriteAsync(data, 0, data.Length); await outStream.FlushAsync(); - outStream.Finish(); + await outStream.FinishAsync(CancellationToken.None); } return memoryStream; } diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs index 0efcf3ac7..b693f205d 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading; using System.Threading.Tasks; using ICSharpCode.SharpZipLib.Tests.TestSupport; using ICSharpCode.SharpZipLib.Zip; @@ -9,11 +10,13 @@ namespace ICSharpCode.SharpZipLib.Tests.Zip [TestFixture] public class ZipStreamAsyncTests { -#if NETCOREAPP3_1_OR_GREATER + [Test] [Category("Zip")] - public async Task WriteZipStreamAsync () + [Category("Async")] + public async Task WriteZipStreamUsingAsync() { +#if NETCOREAPP3_1_OR_GREATER await using var ms = new MemoryStream(); await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false}) @@ -26,22 +29,48 @@ public async Task WriteZipStreamAsync () } ZipTesting.AssertValidZip(ms); +#endif } + + [Test] + [Category("Zip")] + [Category("Async")] + public async Task WriteZipStreamAsync () + { + using var ms = new MemoryStream(); + + using(var outStream = new ZipOutputStream(ms) { IsStreamOwner = false }) + { + await outStream.PutNextEntryAsync(new ZipEntry("FirstFile")); + await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.PutNextEntryAsync(new ZipEntry("SecondFile")); + await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.FinishAsync(CancellationToken.None); + } + + ZipTesting.AssertValidZip(ms); + } + [Test] [Category("Zip")] + [Category("Async")] public async Task WriteZipStreamWithAesAsync() { - await using var ms = new MemoryStream(); + using var ms = new MemoryStream(); var password = "f4ls3p0s1t1v3"; - await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false, Password = password}) + using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false, Password = password}) { await outStream.PutNextEntryAsync(new ZipEntry("FirstFile"){AESKeySize = 256}); await Utils.WriteDummyDataAsync(outStream, 12); await outStream.PutNextEntryAsync(new ZipEntry("SecondFile"){AESKeySize = 256}); await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.FinishAsync(CancellationToken.None); } ZipTesting.AssertValidZip(ms, password); @@ -49,22 +78,25 @@ public async Task WriteZipStreamWithAesAsync() [Test] [Category("Zip")] + [Category("Async")] public async Task WriteZipStreamWithZipCryptoAsync() { - await using var ms = new MemoryStream(); + using var ms = new MemoryStream(); var password = "f4ls3p0s1t1v3"; - await using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false, Password = password}) + using (var outStream = new ZipOutputStream(ms){IsStreamOwner = false, Password = password}) { await outStream.PutNextEntryAsync(new ZipEntry("FirstFile"){AESKeySize = 0}); await Utils.WriteDummyDataAsync(outStream, 12); await outStream.PutNextEntryAsync(new ZipEntry("SecondFile"){AESKeySize = 0}); await Utils.WriteDummyDataAsync(outStream, 12); + + await outStream.FinishAsync(CancellationToken.None); } ZipTesting.AssertValidZip(ms, password, false); } -#endif + } }