diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs index 3dbe98c8d..8ebb75a5b 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs @@ -201,12 +201,12 @@ public Deflater(int level, bool noZlibHeaderOrFooter) /// just created with the same compression level and strategy as it /// had before. /// - public void Reset() + public void Reset(bool deflate64 = false) { state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); totalOut = 0; pending.Reset(); - engine.Reset(); + engine.Reset(deflate64); } /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs index b7c7d2a69..a2f7adc20 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs @@ -142,5 +142,10 @@ public static class DeflaterConstants /// Internal compression engine constant /// public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; + + + public static int WSIZE_64 = 262144; + public static int WMASK_64 = 262143; + public static int MAX_DIST_64 = 65538; } } diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs index 556911c40..55bd4905f 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs @@ -209,10 +209,10 @@ public void SetDictionary(byte[] buffer, int offset, int length) return; } - if (length > DeflaterConstants.MAX_DIST) + if (length > maxDist) { - offset += length - DeflaterConstants.MAX_DIST; - length = DeflaterConstants.MAX_DIST; + offset += length - maxDist; + length = maxDist; } System.Array.Copy(buffer, offset, window, strstart, length); @@ -231,7 +231,7 @@ public void SetDictionary(byte[] buffer, int offset, int length) /// /// Reset internal state /// - public void Reset() + public void Reset(bool deflate64 = false) { huffman.Reset(); adler?.Reset(); @@ -241,17 +241,40 @@ public void Reset() prevAvailable = false; matchLen = DeflaterConstants.MIN_MATCH - 1; + UpdateMode(deflate64); + for (int i = 0; i < DeflaterConstants.HASH_SIZE; i++) { head[i] = 0; } - for (int i = 0; i < DeflaterConstants.WSIZE; i++) + for (int i = 0; i < windowSize; i++) { prev[i] = 0; } } + private void UpdateMode(bool deflate64) + { + if (deflate64) + { + windowSize = DeflaterConstants.WSIZE_64; + maxDist = DeflaterConstants.MAX_DIST_64; + } + else + { + windowSize = DeflaterConstants.WSIZE; + maxDist = DeflaterConstants.MAX_DIST; + } + + var windowFullSize = windowSize * 2; + if (window.Length < windowFullSize) + { + Array.Resize(ref window, windowFullSize); + Array.Resize(ref prev, windowSize); + } + } + /// /// Reset Adler checksum /// @@ -368,7 +391,7 @@ public void FillWindow() /* If the window is almost full and there is insufficient lookahead, * move the upper half to the lower one to make room in the upper half. */ - if (strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + if (strstart >= windowSize + maxDist) { SlideWindow(); } @@ -378,7 +401,7 @@ public void FillWindow() */ if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd) { - int more = 2 * DeflaterConstants.WSIZE - lookahead - strstart; + int more = 2 * windowSize - lookahead - strstart; if (more > inputEnd - inputOff) { @@ -440,24 +463,24 @@ private int InsertString() private void SlideWindow() { - Array.Copy(window, DeflaterConstants.WSIZE, window, 0, DeflaterConstants.WSIZE); - matchStart -= DeflaterConstants.WSIZE; - strstart -= DeflaterConstants.WSIZE; - blockStart -= DeflaterConstants.WSIZE; + Array.Copy(window, windowSize, window, 0, windowSize); + matchStart -= windowSize; + strstart -= windowSize; + blockStart -= windowSize; // Slide the hash table (could be avoided with 32 bit values // at the expense of memory usage). for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) { int m = head[i] & 0xffff; - head[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + head[i] = (short)(m >= windowSize ? (m - windowSize) : 0); } // Slide the prev table. - for (int i = 0; i < DeflaterConstants.WSIZE; i++) + for (int i = 0; i < windowSize; i++) { int m = prev[i] & 0xffff; - prev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + prev[i] = (short)(m >= windowSize ? (m - windowSize) : 0); } } @@ -477,7 +500,7 @@ private bool FindLongestMatch(int curMatch) int scan = strstart; // scanMax is the highest position that we can look at int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, lookahead) - 1; - int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); + int limit = Math.Max(scan - maxDist, 0); byte[] window = this.window; short[] prev = this.prev; @@ -624,7 +647,7 @@ private bool DeflateStored(bool flush, bool finish) int storedLength = strstart - blockStart; if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full - (blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window + (blockStart < windowSize && storedLength >= maxDist) || // Block may move out of window flush) { bool lastBlock = finish; @@ -665,7 +688,7 @@ private bool DeflateFast(bool flush, bool finish) return false; } - if (strstart > 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + if (strstart > 2 * windowSize - DeflaterConstants.MIN_LOOKAHEAD) { /* slide window, as FindLongestMatch needs this. * This should only happen when flushing and the window @@ -678,7 +701,7 @@ private bool DeflateFast(bool flush, bool finish) if (lookahead >= DeflaterConstants.MIN_MATCH && (hashHead = InsertString()) != 0 && strategy != DeflateStrategy.HuffmanOnly && - strstart - hashHead <= DeflaterConstants.MAX_DIST && + strstart - hashHead <= maxDist && FindLongestMatch(hashHead)) { // longestMatch sets matchStart and matchLen @@ -768,7 +791,7 @@ private bool DeflateSlow(bool flush, bool finish) return false; } - if (strstart >= 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + if (strstart >= 2 * windowSize - DeflaterConstants.MIN_LOOKAHEAD) { /* slide window, as FindLongestMatch needs this. * This should only happen when flushing and the window @@ -785,7 +808,7 @@ private bool DeflateSlow(bool flush, bool finish) if (strategy != DeflateStrategy.HuffmanOnly && hashHead != 0 && - strstart - hashHead <= DeflaterConstants.MAX_DIST && + strstart - hashHead <= maxDist && FindLongestMatch(hashHead)) { // longestMatch sets matchStart and matchLen @@ -941,6 +964,9 @@ private bool DeflateSlow(bool flush, bool finish) /// private Adler32 adler; + private int windowSize; + private int maxDist; + #endregion Instance Fields } } diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index 2cc36df22..b538ac079 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -377,7 +377,7 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0, CompressionMethod method = entry.CompressionMethod; // Check that the compression is one that we support - if (method != CompressionMethod.Deflated && method != CompressionMethod.Stored) + if (method != CompressionMethod.Deflated && method != CompressionMethod.Stored && method != CompressionMethod.Deflate64) { throw new NotImplementedException("Compression method not supported"); } @@ -489,9 +489,9 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0, return; crc.Reset(); - if (method == CompressionMethod.Deflated) + if (method == CompressionMethod.Deflated || method == CompressionMethod.Deflate64) { - deflater_.Reset(); + deflater_.Reset(method == CompressionMethod.Deflate64); deflater_.SetLevel(compressionLevel); } } @@ -572,7 +572,7 @@ private async Task FinishCompressionSyncOrAsync(CancellationToken? ct) if (entryIsPassthrough) return; // First finish the deflater, if appropriate - if (curMethod == CompressionMethod.Deflated) + if (curMethod == CompressionMethod.Deflated || curMethod == CompressionMethod.Deflate64) { if (size >= 0) { @@ -634,7 +634,7 @@ internal void WriteEntryFooter(Stream stream) long csize = size; - if (curMethod == CompressionMethod.Deflated && size >= 0) + if ((curMethod == CompressionMethod.Deflated || curMethod == CompressionMethod.Deflate64) && size >= 0) { csize = deflater_.TotalOut; } diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs index e9887172a..b08969d84 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using NUnit.Framework; @@ -108,5 +109,65 @@ internal static void VerifyZipWith7Zip(Stream zipStream, string password) Assert.Warn("Skipping file verification since 7za is not in path"); } } + + internal static IEnumerable<(string, byte[])> GetZipContentsWith7Zip(Stream zipStream, string password) + { + if (TryGet7zBinPath(out string path7z)) + { + Console.WriteLine($"Using 7z path: \"{path7z}\""); + + var inputFile = Path.GetTempFileName(); + using var outputDir = Utils.GetTempDir(); + + Console.WriteLine($"Extracting \"{inputFile}\" to \"{outputDir}\""); + + + try + { + using (var fs = File.OpenWrite(inputFile)) + { + zipStream.Seek(0, SeekOrigin.Begin); + zipStream.CopyTo(fs); + } + + var p = Process.Start(new ProcessStartInfo(path7z, $"e -bb3 -o\"{outputDir.FullName}\" -p{password} \"{inputFile}\"") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + + if (p == null) + { + Assert.Inconclusive("Failed to start 7z process. Skipping!"); + } + if (!p.WaitForExit(2000)) + { + Assert.Warn("Timed out verifying zip file!"); + } + + TestContext.Out.Write(p.StandardOutput.ReadToEnd()); + var errors = p.StandardError.ReadToEnd(); + Assert.IsEmpty(errors, "7z reported errors"); + Assert.AreEqual(0, p.ExitCode, "Extracting archive failed"); + + foreach(var outFile in Directory.EnumerateFiles(outputDir)) + { + yield return ( + Path.GetFileName(outFile), File.ReadAllBytes(outFile) + ); + } + + } + finally + { + File.Delete(inputFile); + } + } + else + { + Assert.Warn("Skipping extraction since 7za is not in path"); + } + } } } diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipDeflate64Tests.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipDeflate64Tests.cs new file mode 100644 index 000000000..10849f820 --- /dev/null +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipDeflate64Tests.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Tests.TestSupport; +using ICSharpCode.SharpZipLib.Zip; +using NUnit.Framework; + +namespace ICSharpCode.SharpZipLib.Tests.Zip +{ + [TestFixture] + public class ZipDeflate64Tests + { + [Test] + [Category("Zip"), Category("Deflate64")] + public void WriteZipStreamWithDeflate64() + { + var contentLength = 2 * 1024 * 1024; + + // Using different seeds so that we can verify that the contents have not been swapped + var seed = 5; + var seed64 = 6; + + using var ms = new MemoryStream(); + + using (var zipOutputStream = new ZipOutputStream(ms) { IsStreamOwner = false }) + { + zipOutputStream.PutNextEntry(new ZipEntry("deflate64.file") + { + CompressionMethod = CompressionMethod.Deflate64, + }); + + Utils.WriteDummyData(zipOutputStream, contentLength, seed64); + + zipOutputStream.PutNextEntry(new ZipEntry("deflate.file") + { + CompressionMethod = CompressionMethod.Deflated, + }); + + Utils.WriteDummyData(zipOutputStream, contentLength, seed); + } + + SevenZipHelper.VerifyZipWith7Zip(ms, null); + foreach (var (name, content) in SevenZipHelper.GetZipContentsWith7Zip(ms, null)) + { + switch (name) + { + case "deflate.file": + Assert.That(content, Is.EqualTo(Utils.GetDummyBytes(contentLength, seed))); + break; + case "deflate64.file": + Assert.That(content, Is.EqualTo(Utils.GetDummyBytes(contentLength, seed64))); + break; + default: + Assert.Fail($"Unexpected file name {name}"); + break; + }; + } + } + } +}