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.