diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
index 6fc17e315..6e76f7f90 100644
--- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
+++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
@@ -128,6 +128,7 @@ public static Task WriteToFileAsync(
{
using var fs = File.Open(destinationFileName, fm);
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
- }
+ },
+ cancellationToken
);
}
diff --git a/src/SharpCompress/Writers/Zip/ZipCentralDirectoryEntry.cs b/src/SharpCompress/Writers/Zip/ZipCentralDirectoryEntry.cs
index 111c7239e..6816859a2 100644
--- a/src/SharpCompress/Writers/Zip/ZipCentralDirectoryEntry.cs
+++ b/src/SharpCompress/Writers/Zip/ZipCentralDirectoryEntry.cs
@@ -48,7 +48,29 @@ internal uint Write(Stream outputStream)
var decompressedvalue = zip64 ? uint.MaxValue : (uint)Decompressed;
var headeroffsetvalue = zip64 ? uint.MaxValue : (uint)HeaderOffset;
var extralength = zip64 ? (2 + 2 + 8 + 8 + 8 + 4) : 0;
- var version = (byte)(zip64 ? 45 : 20); // Version 20 required for deflate/encryption
+
+ // Determine version needed to extract:
+ // - Version 63 for LZMA, PPMd, BZip2, ZStandard (advanced compression methods)
+ // - Version 45 for Zip64 extensions (when Zip64HeaderOffset != 0 or actual sizes require it)
+ // - Version 20 for standard Deflate/None compression
+ byte version;
+ if (
+ compression == ZipCompressionMethod.LZMA
+ || compression == ZipCompressionMethod.PPMd
+ || compression == ZipCompressionMethod.BZip2
+ || compression == ZipCompressionMethod.ZStandard
+ )
+ {
+ version = 63;
+ }
+ else if (zip64 || Zip64HeaderOffset != 0)
+ {
+ version = 45;
+ }
+ else
+ {
+ version = 20;
+ }
var flags = Equals(archiveEncoding.GetEncoding(), Encoding.UTF8)
? HeaderFlags.Efs
diff --git a/tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs b/tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
new file mode 100644
index 000000000..b8451a45c
--- /dev/null
+++ b/tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
@@ -0,0 +1,441 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using SharpCompress.Archives;
+using SharpCompress.Archives.Zip;
+using SharpCompress.Common;
+using SharpCompress.Writers;
+using SharpCompress.Writers.Zip;
+using Xunit;
+
+namespace SharpCompress.Test.Zip;
+
+///
+/// Tests for verifying version consistency between Local File Header (LFH)
+/// and Central Directory File Header (CDFH) when using Zip64.
+///
+public class Zip64VersionConsistencyTests : WriterTests
+{
+ public Zip64VersionConsistencyTests()
+ : base(ArchiveType.Zip) { }
+
+ [Fact]
+ public void Zip64_Small_File_With_UseZip64_Should_Have_Matching_Versions()
+ {
+ // Create a zip with UseZip64=true but with a small file
+ var filename = Path.Combine(SCRATCH2_FILES_PATH, "zip64_version_test.zip");
+
+ if (File.Exists(filename))
+ {
+ File.Delete(filename);
+ }
+
+ // Create archive with UseZip64=true
+ WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate)
+ {
+ LeaveStreamOpen = false,
+ UseZip64 = true,
+ };
+
+ ZipArchive zipArchive = ZipArchive.Create();
+ zipArchive.AddEntry("empty", new MemoryStream());
+ zipArchive.SaveTo(filename, writerOptions);
+
+ // Now read the raw bytes to verify version consistency
+ using var fs = File.OpenRead(filename);
+ using var br = new BinaryReader(fs);
+
+ // Read Local File Header
+ var lfhSignature = br.ReadUInt32();
+ Assert.Equal(0x04034b50u, lfhSignature); // Local file header signature
+
+ var lfhVersion = br.ReadUInt16();
+
+ // Skip to Central Directory
+ // Find Central Directory by searching from the end
+ fs.Seek(-22, SeekOrigin.End); // Min EOCD size
+ var eocdSignature = br.ReadUInt32();
+
+ if (eocdSignature != 0x06054b50u)
+ {
+ // Might have Zip64 EOCD, search backwards
+ fs.Seek(-100, SeekOrigin.End);
+ var buffer = new byte[100];
+ fs.Read(buffer, 0, 100);
+
+ // Find EOCD signature
+ for (int i = buffer.Length - 4; i >= 0; i--)
+ {
+ if (BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(i)) == 0x06054b50u)
+ {
+ fs.Seek(-100 + i, SeekOrigin.End);
+ break;
+ }
+ }
+ }
+
+ // Read EOCD
+ fs.Seek(-22, SeekOrigin.End);
+ br.ReadUInt32(); // EOCD signature
+ br.ReadUInt16(); // disk number
+ br.ReadUInt16(); // disk with central dir
+ br.ReadUInt16(); // entries on this disk
+ br.ReadUInt16(); // total entries
+ br.ReadUInt32(); // central directory size (unused)
+ var cdOffset = br.ReadUInt32();
+
+ // If Zip64, need to read from Zip64 EOCD
+ if (cdOffset == 0xFFFFFFFF)
+ {
+ // Find Zip64 EOCD Locator
+ fs.Seek(-22 - 20, SeekOrigin.End);
+ var z64eocdlSig = br.ReadUInt32();
+ if (z64eocdlSig == 0x07064b50u)
+ {
+ br.ReadUInt32(); // disk number
+ var z64eocdOffset = br.ReadUInt64();
+ br.ReadUInt32(); // total disks
+
+ // Read Zip64 EOCD
+ fs.Seek((long)z64eocdOffset, SeekOrigin.Begin);
+ br.ReadUInt32(); // signature
+ br.ReadUInt64(); // size of EOCD64
+ br.ReadUInt16(); // version made by
+ br.ReadUInt16(); // version needed
+ br.ReadUInt32(); // disk number
+ br.ReadUInt32(); // disk with CD
+ br.ReadUInt64(); // entries on disk
+ br.ReadUInt64(); // total entries
+ br.ReadUInt64(); // CD size
+ cdOffset = (uint)br.ReadUInt64(); // CD offset
+ }
+ }
+
+ // Read Central Directory Header
+ fs.Seek(cdOffset, SeekOrigin.Begin);
+ var cdhSignature = br.ReadUInt32();
+ Assert.Equal(0x02014b50u, cdhSignature); // Central directory header signature
+
+ br.ReadUInt16(); // version made by
+ var cdhVersionNeeded = br.ReadUInt16();
+
+ // The versions should match when UseZip64 is true
+ Assert.Equal(lfhVersion, cdhVersionNeeded);
+ }
+
+ [Fact]
+ public void Zip64_Small_File_Without_UseZip64_Should_Have_Version_20()
+ {
+ // Create a zip without UseZip64
+ var filename = Path.Combine(SCRATCH2_FILES_PATH, "no_zip64_version_test.zip");
+
+ if (File.Exists(filename))
+ {
+ File.Delete(filename);
+ }
+
+ // Create archive without UseZip64
+ WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate)
+ {
+ LeaveStreamOpen = false,
+ UseZip64 = false,
+ };
+
+ ZipArchive zipArchive = ZipArchive.Create();
+ zipArchive.AddEntry("empty", new MemoryStream());
+ zipArchive.SaveTo(filename, writerOptions);
+
+ // Read the raw bytes
+ using var fs = File.OpenRead(filename);
+ using var br = new BinaryReader(fs);
+
+ // Read Local File Header version
+ var lfhSignature = br.ReadUInt32();
+ Assert.Equal(0x04034b50u, lfhSignature);
+ var lfhVersion = br.ReadUInt16();
+
+ // Read Central Directory Header version
+ fs.Seek(-22, SeekOrigin.End);
+ br.ReadUInt32(); // EOCD signature
+ br.ReadUInt16(); // disk number
+ br.ReadUInt16(); // disk with central dir
+ br.ReadUInt16(); // entries on this disk
+ br.ReadUInt16(); // total entries
+ br.ReadUInt32(); // CD size
+ var cdOffset = br.ReadUInt32();
+
+ fs.Seek(cdOffset, SeekOrigin.Begin);
+ var cdhSignature = br.ReadUInt32();
+ Assert.Equal(0x02014b50u, cdhSignature);
+ br.ReadUInt16(); // version made by
+ var cdhVersionNeeded = br.ReadUInt16();
+
+ // Both should be version 20 (or less)
+ Assert.True(lfhVersion <= 20);
+ Assert.Equal(lfhVersion, cdhVersionNeeded);
+ }
+
+ [Fact]
+ public void LZMA_Compression_Should_Use_Version_63()
+ {
+ // Create a zip with LZMA compression
+ var filename = Path.Combine(SCRATCH2_FILES_PATH, "lzma_version_test.zip");
+
+ if (File.Exists(filename))
+ {
+ File.Delete(filename);
+ }
+
+ WriterOptions writerOptions = new ZipWriterOptions(CompressionType.LZMA)
+ {
+ LeaveStreamOpen = false,
+ UseZip64 = false,
+ };
+
+ ZipArchive zipArchive = ZipArchive.Create();
+ var data = new byte[100];
+ new Random(42).NextBytes(data);
+ zipArchive.AddEntry("test.bin", new MemoryStream(data));
+ zipArchive.SaveTo(filename, writerOptions);
+
+ // Read the raw bytes
+ using var fs = File.OpenRead(filename);
+ using var br = new BinaryReader(fs);
+
+ // Read Local File Header version
+ var lfhSignature = br.ReadUInt32();
+ Assert.Equal(0x04034b50u, lfhSignature);
+ var lfhVersion = br.ReadUInt16();
+
+ // Read Central Directory Header version
+ fs.Seek(-22, SeekOrigin.End);
+ br.ReadUInt32(); // EOCD signature
+ br.ReadUInt16(); // disk number
+ br.ReadUInt16(); // disk with central dir
+ br.ReadUInt16(); // entries on this disk
+ br.ReadUInt16(); // total entries
+ br.ReadUInt32(); // CD size
+ var cdOffset = br.ReadUInt32();
+
+ fs.Seek(cdOffset, SeekOrigin.Begin);
+ var cdhSignature = br.ReadUInt32();
+ Assert.Equal(0x02014b50u, cdhSignature);
+ br.ReadUInt16(); // version made by
+ var cdhVersionNeeded = br.ReadUInt16();
+
+ // Both should be version 63 for LZMA
+ Assert.Equal(63, lfhVersion);
+ Assert.Equal(lfhVersion, cdhVersionNeeded);
+ }
+
+ [Fact]
+ public void PPMd_Compression_Should_Use_Version_63()
+ {
+ // Create a zip with PPMd compression
+ var filename = Path.Combine(SCRATCH2_FILES_PATH, "ppmd_version_test.zip");
+
+ if (File.Exists(filename))
+ {
+ File.Delete(filename);
+ }
+
+ WriterOptions writerOptions = new ZipWriterOptions(CompressionType.PPMd)
+ {
+ LeaveStreamOpen = false,
+ UseZip64 = false,
+ };
+
+ ZipArchive zipArchive = ZipArchive.Create();
+ var data = new byte[100];
+ new Random(42).NextBytes(data);
+ zipArchive.AddEntry("test.bin", new MemoryStream(data));
+ zipArchive.SaveTo(filename, writerOptions);
+
+ // Read the raw bytes
+ using var fs = File.OpenRead(filename);
+ using var br = new BinaryReader(fs);
+
+ // Read Local File Header version
+ var lfhSignature = br.ReadUInt32();
+ Assert.Equal(0x04034b50u, lfhSignature);
+ var lfhVersion = br.ReadUInt16();
+
+ // Read Central Directory Header version
+ fs.Seek(-22, SeekOrigin.End);
+ br.ReadUInt32(); // EOCD signature
+ br.ReadUInt16(); // disk number
+ br.ReadUInt16(); // disk with central dir
+ br.ReadUInt16(); // entries on this disk
+ br.ReadUInt16(); // total entries
+ br.ReadUInt32(); // CD size
+ var cdOffset = br.ReadUInt32();
+
+ fs.Seek(cdOffset, SeekOrigin.Begin);
+ var cdhSignature = br.ReadUInt32();
+ Assert.Equal(0x02014b50u, cdhSignature);
+ br.ReadUInt16(); // version made by
+ var cdhVersionNeeded = br.ReadUInt16();
+
+ // Both should be version 63 for PPMd
+ Assert.Equal(63, lfhVersion);
+ Assert.Equal(lfhVersion, cdhVersionNeeded);
+ }
+
+ [Fact]
+ public void Zip64_Multiple_Small_Files_With_UseZip64_Should_Have_Matching_Versions()
+ {
+ // Create a zip with UseZip64=true but with multiple small files
+ var filename = Path.Combine(SCRATCH2_FILES_PATH, "zip64_version_multiple_test.zip");
+
+ if (File.Exists(filename))
+ {
+ File.Delete(filename);
+ }
+
+ WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate)
+ {
+ LeaveStreamOpen = false,
+ UseZip64 = true,
+ };
+
+ ZipArchive zipArchive = ZipArchive.Create();
+ for (int i = 0; i < 5; i++)
+ {
+ var data = new byte[100];
+ new Random(i).NextBytes(data);
+ zipArchive.AddEntry($"file{i}.bin", new MemoryStream(data));
+ }
+ zipArchive.SaveTo(filename, writerOptions);
+
+ // Verify that all entries have matching versions
+ using var fs = File.OpenRead(filename);
+ using var br = new BinaryReader(fs);
+
+ // Read all LFH versions
+ var lfhVersions = new System.Collections.Generic.List();
+ while (true)
+ {
+ var sig = br.ReadUInt32();
+ if (sig == 0x04034b50u) // LFH signature
+ {
+ var version = br.ReadUInt16();
+ lfhVersions.Add(version);
+
+ // Skip rest of LFH
+ br.ReadUInt16(); // flags
+ br.ReadUInt16(); // compression
+ br.ReadUInt32(); // mod time
+ br.ReadUInt32(); // crc
+ br.ReadUInt32(); // compressed size
+ br.ReadUInt32(); // uncompressed size
+ var fnLen = br.ReadUInt16();
+ var extraLen = br.ReadUInt16();
+ fs.Seek(fnLen + extraLen, SeekOrigin.Current);
+
+ // Skip compressed data by reading compressed size from extra field if zip64
+ // For simplicity in this test, we'll just find the next signature
+ var found = false;
+
+ while (fs.Position < fs.Length - 4)
+ {
+ var b = br.ReadByte();
+ if (b == 0x50)
+ {
+ var nextBytes = br.ReadBytes(3);
+ if (
+ (nextBytes[0] == 0x4b && nextBytes[1] == 0x03 && nextBytes[2] == 0x04)
+ || // LFH
+ (nextBytes[0] == 0x4b && nextBytes[1] == 0x01 && nextBytes[2] == 0x02)
+ ) // CDH
+ {
+ fs.Seek(-4, SeekOrigin.Current);
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found)
+ {
+ break;
+ }
+ }
+ else if (sig == 0x02014b50u) // CDH signature
+ {
+ break; // Reached central directory
+ }
+ else
+ {
+ break; // Unknown signature
+ }
+ }
+
+ // Find Central Directory
+ fs.Seek(-22, SeekOrigin.End);
+ br.ReadUInt32(); // EOCD signature
+ br.ReadUInt16(); // disk number
+ br.ReadUInt16(); // disk with central dir
+ br.ReadUInt16(); // entries on this disk
+ var totalEntries = br.ReadUInt16();
+ br.ReadUInt32(); // CD size
+ var cdOffset = br.ReadUInt32();
+
+ // Check if we need Zip64 EOCD
+ if (cdOffset == 0xFFFFFFFF)
+ {
+ fs.Seek(-22 - 20, SeekOrigin.End);
+ var z64eocdlSig = br.ReadUInt32();
+ if (z64eocdlSig == 0x07064b50u)
+ {
+ br.ReadUInt32(); // disk number
+ var z64eocdOffset = br.ReadUInt64();
+ fs.Seek((long)z64eocdOffset, SeekOrigin.Begin);
+ br.ReadUInt32(); // signature
+ br.ReadUInt64(); // size
+ br.ReadUInt16(); // version made by
+ br.ReadUInt16(); // version needed
+ br.ReadUInt32(); // disk number
+ br.ReadUInt32(); // disk with CD
+ br.ReadUInt64(); // entries on disk
+ totalEntries = (ushort)br.ReadUInt64(); // total entries
+ br.ReadUInt64(); // CD size
+ cdOffset = (uint)br.ReadUInt64(); // CD offset
+ }
+ }
+
+ // Read CDH versions
+ fs.Seek(cdOffset, SeekOrigin.Begin);
+ var cdhVersions = new System.Collections.Generic.List();
+ for (int i = 0; i < totalEntries; i++)
+ {
+ var sig = br.ReadUInt32();
+ Assert.Equal(0x02014b50u, sig);
+ br.ReadUInt16(); // version made by
+ var version = br.ReadUInt16();
+ cdhVersions.Add(version);
+
+ // Skip rest of CDH
+ br.ReadUInt16(); // flags
+ br.ReadUInt16(); // compression
+ br.ReadUInt32(); // mod time
+ br.ReadUInt32(); // crc
+ br.ReadUInt32(); // compressed size
+ br.ReadUInt32(); // uncompressed size
+ var fnLen = br.ReadUInt16();
+ var extraLen = br.ReadUInt16();
+ var commentLen = br.ReadUInt16();
+ br.ReadUInt16(); // disk number start
+ br.ReadUInt16(); // internal attributes
+ br.ReadUInt32(); // external attributes
+ br.ReadUInt32(); // LFH offset
+ fs.Seek(fnLen + extraLen + commentLen, SeekOrigin.Current);
+ }
+
+ // Verify all versions match
+ Assert.Equal(lfhVersions.Count, cdhVersions.Count);
+ for (int i = 0; i < lfhVersions.Count; i++)
+ {
+ Assert.Equal(lfhVersions[i], cdhVersions[i]);
+ }
+ }
+}