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 new file mode 100644 index 000000000..a2e30da7f --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs @@ -0,0 +1,130 @@ +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using CT = System.Threading.CancellationToken; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming + +namespace ICSharpCode.SharpZipLib.Core +{ + internal static class ByteOrderStreamExtensions + { + 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)}; + + 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) + }; + + 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) + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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)] + 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)] + internal static int SwappedS32(byte[] bytes) => bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint SwappedU32(byte[] bytes) => (uint) SwappedS32(bytes); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static short SwappedS16(byte[] bytes) => (short)(bytes[0] | bytes[1] << 8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal 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..47de6e26e 100644 --- a/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs +++ b/src/ICSharpCode.SharpZipLib/Core/StreamUtils.cs @@ -1,12 +1,14 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; 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 +16,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 +31,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 +75,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 +120,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 +171,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 +190,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 +274,22 @@ static public void Copy(Stream source, Stream destination, progressHandler(sender, args); } } - - /// - /// Initialise an instance of - /// - private StreamUtils() + + internal static async Task WriteProcToStreamAsync(this Stream targetStream, MemoryStream bufferStream, Action writeProc, CancellationToken ct) { - // Do nothing. + 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/ICSharpCode.SharpZipLib.csproj b/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj index ca37ba1ae..066c4fb43 100644 --- a/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj +++ b/src/ICSharpCode.SharpZipLib/ICSharpCode.SharpZipLib.csproj @@ -40,5 +40,5 @@ Please see https://github.com/icsharpcode/SharpZipLib/wiki/Release-1.3.3 for mor images - + diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs index b6d4025d1..fd4bb47af 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 { @@ -105,10 +107,7 @@ public virtual void Finish() break; } - if (cryptoTransform_ != null) - { - EncryptBlock(buffer_, 0, len); - } + EncryptBlock(buffer_, 0, len); baseOutputStream_.Write(buffer_, 0, len); } @@ -131,6 +130,47 @@ 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 ct) + { + deflater_.Finish(); + while (!deflater_.IsFinished) + { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) + { + break; + } + + EncryptBlock(buffer_, 0, len); + + await baseOutputStream_.WriteAsync(buffer_, 0, len, ct); + } + + if (!deflater_.IsFinished) + { + throw new SharpZipBaseException("Can't deflate all input?"); + } + + await baseOutputStream_.FlushAsync(ct); + + 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. @@ -177,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); } @@ -204,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); } @@ -369,6 +408,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/ZipExtraData.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs index 12e29bb3e..cc2e74490 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs @@ -153,16 +153,15 @@ public ushort 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); @@ -173,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); @@ -181,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); @@ -196,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(); } @@ -342,24 +339,23 @@ public ushort 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; @@ -367,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); } } } @@ -380,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 3bd66ffeb..a07b19f0c 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs @@ -1003,10 +1003,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 (checkCRC && this[entryIndex].Crc != data.Crc) { status.AddError(); @@ -1582,10 +1580,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); } } } @@ -2728,8 +2723,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); } } } @@ -2866,15 +2860,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; @@ -2891,21 +2881,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"); @@ -2920,6 +2910,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) { @@ -3082,10 +3077,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; @@ -3426,13 +3419,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..75f6b72d7 --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs @@ -0,0 +1,597 @@ +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 struct 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 + internal static int WriteLocalHeader(Stream stream, ZipEntry entry, out EntryPatchData patchData, + bool headerInfoAvailable, bool patchEntryHeader, long streamOffset) + { + patchData = new EntryPatchData(); + + stream.WriteLEInt(ZipConstants.LocalHeaderSignature); + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((byte)entry.CompressionMethodForHeader); + stream.WriteLEInt((int)entry.DosTime); + + 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) + patchData.CrcPatchOffset = streamOffset + stream.Position; + + stream.WriteLEInt(0); // Crc + + if (patchEntryHeader) + patchData.SizePatchOffset = streamOffset + 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) + { + 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"); + } + + patchData.SizePatchOffset = 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) + { + stream.Write(name, 0, name.Length); + } + + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) + { + patchData.SizePatchOffset += streamOffset + stream.Position; + } + + if (extra.Length > 0) + { + stream.Write(extra, 0, extra.Length); + } + + return ZipConstants.LocalHeaderBaseSize + name.Length + 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) + { + await stream.WriteProcToStreamAsync(s => WriteZip64EndOfCentralDirectory(s, noOfEntries, sizeEntries, centralDirOffset), 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) + => await stream.WriteProcToStreamAsync(s + => WriteEndOfCentralDirectory(s, noOfEntries, sizeEntries, start, comment), 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(); + } + } + + 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 is null) + ? ZipStrings.ConvertToArray(entry.Flags, entry.Comment) + : Empty.Array(); + + 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"); + } + // 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); + } + 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"); + } + + // 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); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + stream.WriteLEInt((int)entry.Size); + } + + stream.Seek(initialPos, SeekOrigin.Begin); + } + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs index da65630c6..e69de29bb 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs @@ -1,629 +0,0 @@ -using System; -using System.IO; - -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 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); - } - } - - #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 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 an int in little endian byte order. - /// - /// The value to write. - public void WriteLEInt(int value) - { - WriteLEShort(value); - WriteLEShort(value >> 16); - } - - /// - /// 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 long in little endian byte order. - /// - /// The value to write. - public void WriteLELong(long value) - { - WriteLEInt((int)value); - WriteLEInt((int)(value >> 32)); - } - - /// - /// 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)); - } - - #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 the 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 practice 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 79d65f560..7aa3295fe 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Zip { @@ -154,7 +156,7 @@ public UseZip64 UseZip64 /// Defaults to , set to null to disable transforms and use names as supplied. /// public INameTransform NameTransform { get; set; } = new PathTransformer(); - + /// /// Get/set the password used for encryption. /// @@ -217,17 +219,10 @@ private void WriteLeLong(long value) // 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); } /// @@ -244,7 +239,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 @@ -258,6 +253,32 @@ private void TransformEntryName(ZipEntry entry) /// The Compression method specified for the entry is unsupported. /// public void PutNextEntry(ZipEntry 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) { if (entry == null) { @@ -269,11 +290,6 @@ public void PutNextEntry(ZipEntry entry) throw new InvalidOperationException("ZipOutputStream was finished"); } - if (curEntry != null) - { - CloseEntry(); - } - if (entries.Count == int.MaxValue) { throw new ZipException("Too many entries for Zip file"); @@ -365,128 +381,21 @@ public void PutNextEntry(ZipEntry entry) 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 - WriteLeInt(ZipConstants.LocalHeaderSignature); - - WriteLeShort(entry.Version); - WriteLeShort(entry.Flags); - WriteLeShort((byte)entry.CompressionMethodForHeader); - WriteLeInt((int)entry.DosTime); - - // TODO: Refactor header writing. Its done in several places. - if (headerInfoAvailable) - { - WriteLeInt((int)entry.Crc); - if (entry.LocalHeaderRequiresZip64) - { - WriteLeInt(-1); - WriteLeInt(-1); - } - else - { - WriteLeInt((int)entry.CompressedSize + entry.EncryptionOverheadSize); - WriteLeInt((int)entry.Size); - } - } - else - { - if (patchEntryHeader) - { - crcPatchPos = baseOutputStream_.Position; - } - WriteLeInt(0); // Crc - - if (patchEntryHeader) - { - sizePatchPos = baseOutputStream_.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 - } - } - - // 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(); - - WriteLeShort(name.Length); - WriteLeShort(extra.Length); - - if (name.Length > 0) - { - baseOutputStream_.Write(name, 0, name.Length); - } - - if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) - { - sizePatchPos += baseOutputStream_.Position; - } + // Write the local file header + offset += ZipFormat.WriteLocalHeader(stream, entry, out var entryPatchData, + headerInfoAvailable, patchEntryHeader, streamOffset); - if (extra.Length > 0) - { - baseOutputStream_.Write(extra, 0, extra.Length); - } + patchData = entryPatchData; - offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; // Fix offsetOfCentraldir for AES if (entry.AESKeySize > 0) offset += entry.AESOverheadSize; @@ -500,25 +409,47 @@ public void PutNextEntry(ZipEntry entry) 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 ct = default) + { + if (curEntry != null) await CloseEntryAsync(ct); + await baseOutputStream_.WriteProcToStreamAsync(s => + { + PutNextEntry(s, entry, baseOutputStream_.Position); + }, ct); + + if (!entry.IsCrypted) return; + await WriteOutputAsync(GetEntryEncryptionHeader(entry)); } /// @@ -534,6 +465,37 @@ public void PutNextEntry(ZipEntry entry) /// No entry is active. /// public void CloseEntry() + { + WriteEntryFooter(baseOutputStream_); + + // Patch the header if possible + if (patchEntryHeader) + { + patchEntryHeader = false; + ZipFormat.PatchLocalHeaderSync(baseOutputStream_, curEntry, patchData); + } + + entries.Add(curEntry); + curEntry = null; + } + + /// + public async Task CloseEntryAsync(CancellationToken ct) + { + await baseOutputStream_.WriteProcToStreamAsync(WriteEntryFooter, ct); + + // Patch the header if possible + if (patchEntryHeader) + { + patchEntryHeader = false; + await ZipFormat.PatchLocalHeaderAsync(baseOutputStream_, curEntry, patchData, ct); + } + + entries.Add(curEntry); + curEntry = null; + } + + internal void WriteEntryFooter(Stream stream) { if (curEntry == null) { @@ -565,7 +527,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); + stream.Write(AESAuthCode, 0, 10); // Always use 0 as CRC for AE-2 format curEntry.Crc = 0; } @@ -606,91 +568,69 @@ public void CloseEntry() 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)); + stream.WriteLEInt(ZipConstants.DataDescriptorSignature); + stream.WriteLEInt(unchecked((int)curEntry.Crc)); if (curEntry.LocalHeaderRequiresZip64) { - WriteLeLong(curEntry.CompressedSize); - WriteLeLong(curEntry.Size); + stream.WriteLELong(curEntry.CompressedSize); + stream.WriteLELong(curEntry.Size); offset += ZipConstants.Zip64DataDescriptorSize; } else { - WriteLeInt((int)curEntry.CompressedSize); - WriteLeInt((int)curEntry.Size); + stream.WriteLEInt((int)curEntry.CompressedSize); + stream.WriteLEInt((int)curEntry.Size); offset += ZipConstants.DataDescriptorSize; } } - - entries.Add(curEntry); - curEntry = null; } - /// - /// 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. /// - private 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(); _aesRnd.GetBytes(salt); - int blockSize = entry.AESKeySize / 8; // bits to bytes cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); - pwdVerifier = ((ZipAESTransform)cryptoTransform_).PwdVerifier; - } - private void WriteEncryptionHeader(long crcValue) + 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; + } + + private byte[] CreateZipCryptoHeader(long crcValue) { offset += ZipConstants.CryptoHeaderSize; - InitializePassword(Password); + InitializeZipCryptoPassword(Password); byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; using (var rng = new RNGCryptoServiceProvider()) @@ -701,47 +641,21 @@ private void WriteEncryptionHeader(long crcValue) cryptBuffer[11] = (byte)(crcValue >> 24); EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); - baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length); - } - 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); + return cryptBuffer; } - - // Replaces WriteEncryptionHeader for AES - // - private void WriteAESHeader(ZipEntry entry) + + /// + /// Initializes encryption keys based on given . + /// + /// The password. + private void InitializeZipCryptoPassword(string password) { - 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. - baseOutputStream_.Write(salt, 0, salt.Length); - baseOutputStream_.Write(pwdVerifier, 0, pwdVerifier.Length); + var pkManaged = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password)); + cryptoTransform_ = pkManaged.CreateEncryptor(key, null); } - + /// /// Writes the given buffer to the current entry. /// @@ -849,144 +763,48 @@ 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); - } + sizeEntries += ZipFormat.WriteEndEntry(baseOutputStream_, entry); + } - if (entry.Offset >= 0xffffffff) - { - ed.AddLeLong(entry.Offset); - } + ZipFormat.WriteEndOfCentralDirectory(baseOutputStream_, numEntries, sizeEntries, offset, zipComment); - ed.AddNewEntry(1); - } - else - { - ed.Delete(1); - } + entries = null; + } - if (entry.AESKeySize > 0) + /// > + public override async Task FinishAsync(CancellationToken ct) + { + using (var ms = new MemoryStream()) + { + if (entries == null) { - AddExtraDataAES(entry, ed); + return; } - byte[] extra = ed.GetEntryData(); - - byte[] entryComment = - (entry.Comment != null) ? - ZipStrings.ConvertToArray(entry.Flags, entry.Comment) : - Empty.Array(); - if (entryComment.Length > 0xffff) + if (curEntry != null) { - throw new ZipException("Comment too long."); + await CloseEntryAsync(ct); } - WriteLeShort(name.Length); - WriteLeShort(extra.Length); - WriteLeShort(entryComment.Length); - WriteLeShort(0); // disk number - WriteLeShort(0); // internal file attributes - // external file attributes + long numEntries = entries.Count; + long sizeEntries = 0; - if (entry.ExternalFileAttributes != -1) + foreach (var entry in entries) { - WriteLeInt(entry.ExternalFileAttributes); - } - else - { - if (entry.IsDirectory) - { // mark entry as directory (from nikolam.AT.perfectinfo.com) - WriteLeInt(16); - } - else + await baseOutputStream_.WriteProcToStreamAsync(ms, s => { - WriteLeInt(0); - } - } - - if (entry.Offset >= uint.MaxValue) - { - WriteLeInt(-1); - } - else - { - WriteLeInt((int)entry.Offset); + sizeEntries += ZipFormat.WriteEndEntry(s, entry); + }, ct); } - 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); - } + await baseOutputStream_.WriteProcToStreamAsync(ms, s + => ZipFormat.WriteEndOfCentralDirectory(s, numEntries, sizeEntries, offset, zipComment), + ct); - sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + entries = null; } - - using (ZipHelperStream zhs = new ZipHelperStream(baseOutputStream_)) - { - zhs.WriteEndOfCentralDirectory(numEntries, sizeEntries, offset, zipComment); - } - - entries = null; } /// @@ -1047,14 +865,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/Base/InflaterDeflaterTests.cs b/test/ICSharpCode.SharpZipLib.Tests/Base/InflaterDeflaterTests.cs index 9df9319b4..e9ba0ad77 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 @@ -113,7 +114,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/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 12183fcdd..4a46e84f2 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj +++ b/test/ICSharpCode.SharpZipLib.Tests/ICSharpCode.SharpZipLib.Tests.csproj @@ -5,7 +5,10 @@ netcoreapp3.1;net46 - 8 + true + ..\..\assets\ICSharpCode.SharpZipLib.snk + true + 8.0 @@ -25,4 +28,10 @@ + + + ICSharpCode.SharpZipLib.snk + + + diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs index 179202b44..d0d2f2175 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace ICSharpCode.SharpZipLib.Tests.TestSupport { @@ -76,6 +78,12 @@ public static byte[] GetDummyBytes(int size, int seed = DefaultSeed) random.NextBytes(bytes); return bytes; } + + public static async Task WriteDummyDataAsync(Stream stream, int size = -1) + { + var bytes = GetDummyBytes(size); + await stream.WriteAsync(bytes, 0, bytes.Length); + } /// /// Returns a file reference with bytes of dummy data written to it 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/ZipFileHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs index f2b3e7859..2d641370c 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs @@ -913,7 +913,10 @@ 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) => { + 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..b693f205d --- /dev/null +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipStreamAsyncTests.cs @@ -0,0 +1,102 @@ +using System.IO; +using System.Threading; +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 + { + + [Test] + [Category("Zip")] + [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}) + { + 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); +#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() + { + using var ms = new MemoryStream(); + var password = "f4ls3p0s1t1v3"; + + 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); + } + + [Test] + [Category("Zip")] + [Category("Async")] + public async Task WriteZipStreamWithZipCryptoAsync() + { + using var ms = new MemoryStream(); + var password = "f4ls3p0s1t1v3"; + + 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); + } + + } +}