Skip to content

Commit 106b02f

Browse files
Set version in ZIP local header to ZIP64 when file offset is >4GB (#102053) (#103006)
* ZipArchiveEntry didn't set ZIP64 in local headers for small files if their offset are > 4GB. * Added System.IO.Compression and System.IO.Packaging tests. --------- Co-authored-by: Gan Keyu <gankeyu@hotmail.com>
1 parent 4d4640f commit 106b02f

File tree

7 files changed

+306
-101
lines changed

7 files changed

+306
-101
lines changed

src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ internal void WriteCentralDirectoryFileHeader()
469469

470470
bool zip64Needed = false;
471471

472-
if (SizesTooLarge()
472+
if (AreSizesTooLarge
473473
#if DEBUG_FORCE_ZIP64
474474
|| _archive._forceZip64
475475
#endif
@@ -490,7 +490,7 @@ internal void WriteCentralDirectoryFileHeader()
490490
}
491491

492492

493-
if (_offsetOfLocalHeader > uint.MaxValue
493+
if (IsOffsetTooLarge
494494
#if DEBUG_FORCE_ZIP64
495495
|| _archive._forceZip64
496496
#endif
@@ -797,7 +797,11 @@ private bool IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out st
797797
return true;
798798
}
799799

800-
private bool SizesTooLarge() => _compressedSize > uint.MaxValue || _uncompressedSize > uint.MaxValue;
800+
private bool AreSizesTooLarge => _compressedSize > uint.MaxValue || _uncompressedSize > uint.MaxValue;
801+
802+
private bool IsOffsetTooLarge => _offsetOfLocalHeader > uint.MaxValue;
803+
804+
private bool ShouldUseZIP64 => AreSizesTooLarge || IsOffsetTooLarge;
801805

802806
// return value is true if we allocated an extra field for 64 bit headers, un/compressed size
803807
private bool WriteLocalFileHeader(bool isEmptyFile)
@@ -813,6 +817,9 @@ private bool WriteLocalFileHeader(bool isEmptyFile)
813817
bool zip64Used = false;
814818
uint compressedSizeTruncated, uncompressedSizeTruncated;
815819

820+
// save offset
821+
_offsetOfLocalHeader = writer.BaseStream.Position;
822+
816823
// if we already know that we have an empty file don't worry about anything, just do a straight shot of the header
817824
if (isEmptyFile)
818825
{
@@ -840,7 +847,7 @@ private bool WriteLocalFileHeader(bool isEmptyFile)
840847
{
841848
// We are in seekable mode so we will not need to write a data descriptor
842849
_generalPurposeBitFlag &= ~BitFlagValues.DataDescriptor;
843-
if (SizesTooLarge()
850+
if (ShouldUseZIP64
844851
#if DEBUG_FORCE_ZIP64
845852
|| (_archive._forceZip64 && _archive.Mode == ZipArchiveMode.Update)
846853
#endif
@@ -865,9 +872,6 @@ private bool WriteLocalFileHeader(bool isEmptyFile)
865872
}
866873
}
867874

868-
// save offset
869-
_offsetOfLocalHeader = writer.BaseStream.Position;
870-
871875
// calculate extra field. if zip64 stuff + original extraField aren't going to fit, dump the original extraField, because this is more important
872876
int bigExtraFieldLength = (zip64Used ? zip64ExtraField.TotalSize : 0)
873877
+ (_lhUnknownExtraFields != null ? ZipGenericExtraField.TotalSize(_lhUnknownExtraFields) : 0);
@@ -964,7 +968,7 @@ private void WriteCrcAndSizesInLocalHeader(bool zip64HeaderUsed)
964968
long finalPosition = _archive.ArchiveStream.Position;
965969
BinaryWriter writer = new BinaryWriter(_archive.ArchiveStream);
966970

967-
bool zip64Needed = SizesTooLarge()
971+
bool zip64Needed = ShouldUseZIP64
968972
#if DEBUG_FORCE_ZIP64
969973
|| _archive._forceZip64
970974
#endif
@@ -1048,7 +1052,7 @@ private void WriteDataDescriptor()
10481052

10491053
writer.Write(ZipLocalFileHeader.DataDescriptorSignature);
10501054
writer.Write(_crc32);
1051-
if (SizesTooLarge())
1055+
if (AreSizesTooLarge)
10521056
{
10531057
writer.Write(_compressedSize);
10541058
writer.Write(_uncompressedSize);
Lines changed: 105 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,129 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Generic;
5-
using System.Linq;
4+
using System.Reflection;
65
using Xunit;
76

8-
namespace System.IO.Compression.Tests
7+
namespace System.IO.Compression.Tests;
8+
9+
[Collection(nameof(DisableParallelization))]
10+
public class zip_LargeFiles : ZipFileTestBase
911
{
10-
[Collection(nameof(DisableParallelization))]
11-
public class zip_LargeFiles : ZipFileTestBase
12+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes
13+
[OuterLoop("It requires almost 12 GB of free disk space")]
14+
public static void UnzipOver4GBZipFile()
1215
{
13-
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes
14-
[OuterLoop("It requires almost 12 GB of free disk space")]
15-
public static void UnzipOver4GBZipFile()
16+
byte[] buffer = GC.AllocateUninitializedArray<byte>(1_000_000_000); // 1 GB
17+
18+
string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB.zip");
19+
DirectoryInfo tempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "over4GB"));
20+
21+
try
22+
{
23+
for (byte i = 0; i < 6; i++)
24+
{
25+
File.WriteAllBytes(Path.Combine(tempDir.FullName, $"{i}.test"), buffer);
26+
}
27+
28+
ZipFile.CreateFromDirectory(tempDir.FullName, zipArchivePath, CompressionLevel.NoCompression, includeBaseDirectory: false);
29+
30+
using ZipArchive zipArchive = ZipFile.OpenRead(zipArchivePath);
31+
foreach (ZipArchiveEntry entry in zipArchive.Entries)
32+
{
33+
using Stream entryStream = entry.Open();
34+
35+
Assert.True(entryStream.CanRead);
36+
Assert.Equal(buffer.Length, entryStream.Length);
37+
}
38+
}
39+
finally
1640
{
17-
byte[] buffer = GC.AllocateUninitializedArray<byte>(1_000_000_000); // 1 GB
41+
File.Delete(zipArchivePath);
42+
43+
tempDir.Delete(recursive: true);
44+
}
45+
}
46+
47+
private static void FillWithHardToCompressData(byte[] buffer)
48+
{
49+
Random.Shared.NextBytes(buffer);
50+
}
1851

19-
string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB.zip");
20-
DirectoryInfo tempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "over4GB"));
52+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes
53+
[OuterLoop("It requires 5~6 GB of free disk space and a lot of CPU time for compressed tests")]
54+
[InlineData(false)]
55+
[InlineData(true)]
56+
public static void CheckZIP64VersionIsSet_ForSmallFilesAfterBigFiles(bool isCompressed)
57+
{
58+
// issue #94899
59+
60+
CompressionLevel compressLevel = isCompressed ? CompressionLevel.Optimal : CompressionLevel.NoCompression;
61+
byte[] smallBuffer = GC.AllocateUninitializedArray<byte>(1000);
62+
byte[] largeBuffer = GC.AllocateUninitializedArray<byte>(1_000_000_000); // ~1 GB
63+
string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB.zip");
64+
string LargeFileName = "largefile";
65+
string SmallFileName = "smallfile";
66+
uint ZipLocalFileHeader_OffsetToVersionFromHeaderStart = 4;
67+
ushort Zip64Version = 45;
2168

22-
try
69+
try
70+
{
71+
using FileStream fs = File.Open(zipArchivePath, FileMode.Create, FileAccess.ReadWrite);
72+
73+
// Create
74+
using (ZipArchive archive = new(fs, ZipArchiveMode.Create, true))
2375
{
24-
for (byte i = 0; i < 6; i++)
76+
ZipArchiveEntry file = archive.CreateEntry(LargeFileName, compressLevel);
77+
78+
using (Stream stream = file.Open())
2579
{
26-
File.WriteAllBytes(Path.Combine(tempDir.FullName, $"{i}.test"), buffer);
80+
// Write 5GB of data
81+
for (var i = 0; i < 5; i++)
82+
{
83+
if (isCompressed)
84+
{
85+
FillWithHardToCompressData(largeBuffer);
86+
}
87+
88+
stream.Write(largeBuffer);
89+
}
2790
}
2891

29-
ZipFile.CreateFromDirectory(tempDir.FullName, zipArchivePath, CompressionLevel.NoCompression, includeBaseDirectory: false);
92+
file = archive.CreateEntry(SmallFileName, compressLevel);
3093

31-
using ZipArchive zipArchive = ZipFile.OpenRead(zipArchivePath);
32-
foreach (ZipArchiveEntry entry in zipArchive.Entries)
94+
using (Stream stream = file.Open())
3395
{
34-
using Stream entryStream = entry.Open();
35-
36-
Assert.True(entryStream.CanRead);
37-
Assert.Equal(buffer.Length, entryStream.Length);
96+
stream.Write(smallBuffer);
3897
}
3998
}
40-
finally
99+
100+
fs.Position = 0;
101+
102+
// Validate
103+
using (ZipArchive archive = new(fs, ZipArchiveMode.Read))
41104
{
42-
File.Delete(zipArchivePath);
105+
using var reader = new BinaryReader(fs);
43106

44-
tempDir.Delete(recursive: true);
107+
FieldInfo offsetOfLHField = typeof(ZipArchiveEntry).GetField("_offsetOfLocalHeader", BindingFlags.NonPublic | BindingFlags.Instance);
108+
109+
if (offsetOfLHField is null || offsetOfLHField.FieldType != typeof(long))
110+
{
111+
Assert.Fail("Cannot find the private field of _offsetOfLocalHeader in ZipArchiveEntry or the type is not long. Code may be changed after the test is written.");
112+
}
113+
114+
foreach (ZipArchiveEntry entry in archive.Entries)
115+
{
116+
fs.Position = (long)offsetOfLHField.GetValue(entry) + ZipLocalFileHeader_OffsetToVersionFromHeaderStart;
117+
ushort versionNeeded = reader.ReadUInt16();
118+
119+
// Version is not ZIP64 for files with Local Header at >4GB offset.
120+
Assert.Equal(Zip64Version, versionNeeded);
121+
}
45122
}
46123
}
124+
finally
125+
{
126+
File.Delete(zipArchivePath);
127+
}
47128
}
48129
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO.Compression;
5+
using System.Net.Mime;
6+
using System.Reflection;
7+
using Xunit;
8+
9+
namespace System.IO.Packaging.Tests;
10+
11+
public partial class LargeFilesTests
12+
{
13+
private static void FillWithHardToCompressData(byte[] buffer)
14+
{
15+
Random.Shared.NextBytes(buffer);
16+
}
17+
18+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes
19+
[InlineData(false)]
20+
[InlineData(true)]
21+
[OuterLoop("It requires 5~6 GB of free disk space and a lot of CPU time for compressed tests")]
22+
public static void CheckZIP64VersionIsSet_ForSmallFilesAfterBigFiles(bool isCompressed)
23+
{
24+
// issue #94899
25+
26+
CompressionOption compressionOption = isCompressed ? CompressionOption.Normal : CompressionOption.NotCompressed;
27+
byte[] smallBuffer = GC.AllocateUninitializedArray<byte>(1000);
28+
byte[] largeBuffer = GC.AllocateUninitializedArray<byte>(1_000_000_000); // ~1 GB
29+
string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB.zip");
30+
Uri largePartUri = PackUriHelper.CreatePartUri(new Uri("large.bin", UriKind.Relative));
31+
Uri smallPartUri = PackUriHelper.CreatePartUri(new Uri("small.bin", UriKind.Relative));
32+
uint ZipLocalFileHeader_OffsetToVersionFromHeaderStart = 4;
33+
ushort Zip64Version = 45;
34+
35+
try
36+
{
37+
using FileStream fs = File.Open(zipArchivePath, FileMode.Create, FileAccess.ReadWrite);
38+
39+
// Create
40+
using (Package package = Package.Open(fs, FileMode.Create, FileAccess.Write))
41+
{
42+
PackagePart partLarge = package.CreatePart(largePartUri, MediaTypeNames.Application.Octet, compressionOption);
43+
44+
using (Stream streamLarge = partLarge.GetStream())
45+
{
46+
// Write 5GB of data
47+
48+
for (var i = 0; i < 5; i++)
49+
{
50+
if (isCompressed)
51+
{
52+
FillWithHardToCompressData(largeBuffer);
53+
}
54+
55+
streamLarge.Write(largeBuffer);
56+
}
57+
}
58+
59+
PackagePart partSmall = package.CreatePart(smallPartUri, MediaTypeNames.Application.Octet, compressionOption);
60+
61+
using (Stream streamSmall = partSmall.GetStream())
62+
{
63+
streamSmall.Write(smallBuffer);
64+
}
65+
}
66+
67+
68+
fs.Position = 0;
69+
70+
// Validate
71+
using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read))
72+
{
73+
using var reader = new BinaryReader(fs);
74+
75+
FieldInfo offsetOfLHField = typeof(ZipArchiveEntry).GetField("_offsetOfLocalHeader", BindingFlags.NonPublic | BindingFlags.Instance);
76+
77+
if (offsetOfLHField is null || offsetOfLHField.FieldType != typeof(long))
78+
{
79+
Assert.Fail("Cannot find the private field of _offsetOfLocalHeader in ZipArchiveEntry or the type is not long. Code may be changed after the test is written.");
80+
}
81+
82+
foreach (ZipArchiveEntry entry in archive.Entries)
83+
{
84+
fs.Position = (long)offsetOfLHField.GetValue(entry) + ZipLocalFileHeader_OffsetToVersionFromHeaderStart;
85+
ushort versionNeeded = reader.ReadUInt16();
86+
87+
// Version is not ZIP64 for files with Local Header at >4GB offset.
88+
Assert.Equal(Zip64Version, versionNeeded);
89+
}
90+
}
91+
}
92+
finally
93+
{
94+
File.Delete(zipArchivePath);
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)