mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 23:15:20 +00:00
Merge pull request #1023 from adamhathcock/copilot/fix-zip64-validation-issue
Fix version mismatch between Local File Header and Central Directory File Header in Zip archives
This commit is contained in:
@@ -128,6 +128,7 @@ public static class IArchiveEntryExtensions
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,29 @@ internal class ZipCentralDirectoryEntry
|
||||
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
|
||||
|
||||
441
tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
Normal file
441
tests/SharpCompress.Test/Zip/Zip64VersionConsistencyTests.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for verifying version consistency between Local File Header (LFH)
|
||||
/// and Central Directory File Header (CDFH) when using Zip64.
|
||||
/// </summary>
|
||||
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<ushort>();
|
||||
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<ushort>();
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user