diff --git a/src/libraries/System.IO.Compression/tests/Manual/ManualTests.Net.cs b/src/libraries/System.IO.Compression/tests/Manual/ManualTests.Net.cs new file mode 100644 index 00000000000000..3e35c4350b4427 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/Manual/ManualTests.Net.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using Xunit; + +namespace System.IO.Compression.Tests; + +public class ManualTests +{ + [Fact] + public static void GenerateZip64File() + { + // This test has the purpose of generating a large zip file containing the Zip64 bug fix introduced in https://github.com/dotnet/runtime/pull/102053 + // The file can later be used in the VerifyZip64FixInNetFramework test method in ManualTests.NetFramework.cs to confirm that it can read it correctly. + + const ushort Zip64Version = 45; + const uint ZipLocalFileHeader_OffsetToVersionFromHeaderStart = 4; + byte[] largeBuffer = GC.AllocateUninitializedArray(1_000_000_000); // 1 GB + string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB_WithoutBug.zip"); + + using FileStream fs = File.Open(zipArchivePath, FileMode.Create, FileAccess.ReadWrite); + + // Create + using (ZipArchive archive = new(fs, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry file = archive.CreateEntry("file.txt", CompressionLevel.NoCompression); + + using (Stream stream = file.Open()) + { + // Write 5GB of data + for (var i = 0; i < 5; i++) + { + stream.Write(largeBuffer); + } + } + } + Assert.True(fs.Length > int.MaxValue, $"File size is not big enough to test the Zip64 fix: {fs.Length} vs {int.MaxValue}"); + + fs.Position = 0; + + // Create an archive using .NET with the fix, to later use it in .NET Framework + using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: true)) + { + FieldInfo offsetOfLHField = typeof(ZipArchiveEntry).GetField("_offsetOfLocalHeader", BindingFlags.NonPublic | BindingFlags.Instance); + + if (offsetOfLHField is null || offsetOfLHField.FieldType != typeof(long)) + { + 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."); + } + + ZipArchiveEntry entry = zip.Entries.First(); + + long currentPosition = fs.Position; + + // Confirm it's set to the correct value + using (BinaryReader reader = new(fs, Encoding.UTF8, leaveOpen: true)) + { + fs.Position = (long)offsetOfLHField.GetValue(entry) + ZipLocalFileHeader_OffsetToVersionFromHeaderStart; + ushort version = reader.ReadUInt16(); + Assert.Equal(Zip64Version, version); + } + } + + Console.WriteLine($"Zip file location: {zipArchivePath}"); + } +} \ No newline at end of file diff --git a/src/libraries/System.IO.Compression/tests/Manual/ManualTests.NetFramework.cs b/src/libraries/System.IO.Compression/tests/Manual/ManualTests.NetFramework.cs new file mode 100644 index 00000000000000..cfcaf040359ccd --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/Manual/ManualTests.NetFramework.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Text; +using Xunit; + +namespace System.IO.Compression.Tests; + +public class ManualTests +{ + [Fact] + public static void VerifyZip64FixInNetFramework() + { + // This test has the purpose of verifying that .NET Framework can read a zip file generated with the Zip64 fix introduced in .NET. + // The zip file with the fix needs to be manually generated by the GenerateZip64File test method in ManualTests.Net.cs. + + string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB_WithoutBug.zip"); + Assert.True(File.Exists(zipArchivePath)); + + using FileStream fs = File.OpenRead(zipArchivePath); + + // Open archive to verify that we can still read an archive with the buggy version + using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read)) + { + Assert.Equal(1, zip.Entries.Count); + ZipArchiveEntry entry = zip.Entries.First(); + Assert.Equal("file.txt", entry.Name); + Assert.Equal(5_000_000_000, entry.Length); + } + } +} diff --git a/src/libraries/System.IO.Compression/tests/Manual/System.IO.Compression.Manual.Net.Tests.csproj b/src/libraries/System.IO.Compression/tests/Manual/System.IO.Compression.Manual.Net.Tests.csproj new file mode 100644 index 00000000000000..ced81689edb220 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/Manual/System.IO.Compression.Manual.Net.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(NetCoreAppCurrent) + + + + + + + \ No newline at end of file diff --git a/src/libraries/System.IO.Compression/tests/Manual/System.IO.Compression.Manual.NetFramework.Tests.csproj b/src/libraries/System.IO.Compression/tests/Manual/System.IO.Compression.Manual.NetFramework.Tests.csproj new file mode 100644 index 00000000000000..f8d48797b23b4d --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/Manual/System.IO.Compression.Manual.NetFramework.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(NetFrameworkCurrent) + + + + + + + + + + + \ No newline at end of file diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_LargeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_LargeFiles.cs index b4623e653af84d..551d118face518 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_LargeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_LargeFiles.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using System.Reflection; +using System.Text; using Xunit; namespace System.IO.Compression.Tests; @@ -9,6 +11,23 @@ namespace System.IO.Compression.Tests; [Collection(nameof(DisableParallelization))] public class zip_LargeFiles : ZipFileTestBase { + private const ushort Zip64Version = 45; + private const uint ZipLocalFileHeader_OffsetToVersionFromHeaderStart = 4; + + private static void FillWithHardToCompressData(byte[] buffer) => Random.Shared.NextBytes(buffer); + + private static FieldInfo GetOffsetOfLHField() + { + FieldInfo offsetOfLHField = typeof(ZipArchiveEntry).GetField("_offsetOfLocalHeader", BindingFlags.NonPublic | BindingFlags.Instance); + + if (offsetOfLHField is null || offsetOfLHField.FieldType != typeof(long)) + { + 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."); + } + + return offsetOfLHField; + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes [OuterLoop("It requires almost 12 GB of free disk space")] public static void UnzipOver4GBZipFile() @@ -44,11 +63,6 @@ public static void UnzipOver4GBZipFile() } } - private static void FillWithHardToCompressData(byte[] buffer) - { - Random.Shared.NextBytes(buffer); - } - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes [OuterLoop("It requires 5~6 GB of free disk space and a lot of CPU time for compressed tests")] [InlineData(false)] @@ -63,8 +77,6 @@ public static void CheckZIP64VersionIsSet_ForSmallFilesAfterBigFiles(bool isComp string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB.zip"); string LargeFileName = "largefile"; string SmallFileName = "smallfile"; - uint ZipLocalFileHeader_OffsetToVersionFromHeaderStart = 4; - ushort Zip64Version = 45; try { @@ -104,12 +116,7 @@ public static void CheckZIP64VersionIsSet_ForSmallFilesAfterBigFiles(bool isComp { using var reader = new BinaryReader(fs); - FieldInfo offsetOfLHField = typeof(ZipArchiveEntry).GetField("_offsetOfLocalHeader", BindingFlags.NonPublic | BindingFlags.Instance); - - if (offsetOfLHField is null || offsetOfLHField.FieldType != typeof(long)) - { - 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."); - } + FieldInfo offsetOfLHField = GetOffsetOfLHField(); foreach (ZipArchiveEntry entry in archive.Entries) { @@ -126,4 +133,85 @@ public static void CheckZIP64VersionIsSet_ForSmallFilesAfterBigFiles(bool isComp File.Delete(zipArchivePath); } } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsSpeedOptimized), nameof(PlatformDetection.Is64BitProcess))] // don't run it on slower runtimes + [OuterLoop("It requires around 6 GB of free disk space")] + public static void CompatZip64BeforeAndAfterFix() + { + // This test has the purpose of confirming that ZipArchive can still process a zip file that was created + // with these APIs before the Zip64 bug was fixed: https://github.com/dotnet/runtime/pull/102053 + + ushort buggyZip64Version = 20; + byte[] largeBuffer = GC.AllocateUninitializedArray(1_000_000_000); // 1 GB + string zipArchivePath = Path.Combine(Path.GetTempPath(), "over4GB.zip"); + + try + { + using FileStream fs = File.Open(zipArchivePath, FileMode.Create, FileAccess.ReadWrite); + + // Create + using (ZipArchive archive = new(fs, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry file = archive.CreateEntry("file.txt", CompressionLevel.NoCompression); + + using (Stream stream = file.Open()) + { + // Write 5GB of data + for (var i = 0; i < 5; i++) + { + stream.Write(largeBuffer); + } + } + } + Assert.True(fs.Length > int.MaxValue, $"File size is not big enough to test the Zip64 fix: {fs.Length} vs {int.MaxValue}"); + + fs.Position = 0; + + // Open archive to modify the bit as it used to look before fix + using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: true)) + { + FieldInfo offsetOfLHField = GetOffsetOfLHField(); + + ZipArchiveEntry entry = zip.Entries.First(); + + long currentPosition = fs.Position; + + // Confirm it's initially set to the correct value + using (BinaryReader reader = new(fs, Encoding.UTF8, leaveOpen: true)) + { + fs.Position = (long)offsetOfLHField.GetValue(entry) + ZipLocalFileHeader_OffsetToVersionFromHeaderStart; + ushort version = reader.ReadUInt16(); + Assert.Equal(Zip64Version, version); + } + + fs.Position = currentPosition; + + // Change it to the value that a version of .NET previous to the fix would have written + using (BinaryWriter writer = new(fs, Encoding.UTF8, leaveOpen: true)) + { + fs.Position = (long)offsetOfLHField.GetValue(entry) + ZipLocalFileHeader_OffsetToVersionFromHeaderStart; + writer.Write(buggyZip64Version); + } + } + + fs.Position = 0; + + // Open archive to verify that we can still read an archive with the buggy version + using (ZipArchive zip = new ZipArchive(fs, ZipArchiveMode.Read)) + { + FieldInfo offsetOfLHField = GetOffsetOfLHField(); + ZipArchiveEntry entry = zip.Entries.First(); + + // Confirm it's still set to the old buggy value (to prove we could still read a malformed file) + using BinaryReader reader = new(fs, Encoding.UTF8); + fs.Position = (long)offsetOfLHField.GetValue(entry) + ZipLocalFileHeader_OffsetToVersionFromHeaderStart; + ushort version = reader.ReadUInt16(); + Assert.Equal(buggyZip64Version, version); + } + } + finally + { + File.Delete(zipArchivePath); + } + } }