diff --git a/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs b/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs index 6adf468d0a12a7..e19760eaeb9ade 100644 --- a/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs +++ b/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs @@ -24,11 +24,7 @@ internal sealed class NoSyncCallsStream : Stream public override long Position { get => _s.Position; set => _s.Position = value; } public override int ReadTimeout { get => _s.ReadTimeout; set => _s.ReadTimeout = value; } public override int WriteTimeout { get => _s.WriteTimeout; set => _s.WriteTimeout = value; } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => _s.BeginRead(buffer, offset, count, callback, state); - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => _s.BeginWrite(buffer, offset, count, callback, state); public override void Close() => _s.Close(); - public override int EndRead(IAsyncResult asyncResult) => _s.EndRead(asyncResult); - public override void EndWrite(IAsyncResult asyncResult) => _s.EndWrite(asyncResult); public override bool Equals(object? obj) => _s.Equals(obj); public override int GetHashCode() => _s.GetHashCode(); public override int ReadByte() => _s.ReadByte(); @@ -37,6 +33,10 @@ internal sealed class NoSyncCallsStream : Stream public override string? ToString() => _s.ToString(); // Sync + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => + IsRestrictionEnabled ? throw new InvalidOperationException() : _s.BeginRead(buffer, offset, count, callback, state); + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => + IsRestrictionEnabled ? throw new InvalidOperationException() : _s.BeginRead(buffer, offset, count, callback, state); public override void CopyTo(Stream destination, int bufferSize) { if (IsRestrictionEnabled) @@ -60,6 +60,18 @@ protected override void Dispose(bool disposing) _s.Dispose(); } } + public override int EndRead(IAsyncResult asyncResult) => IsRestrictionEnabled ? throw new InvalidOperationException() : _s.EndRead(asyncResult); + public override void EndWrite(IAsyncResult asyncResult) + { + if (IsRestrictionEnabled) + { + throw new InvalidOperationException(); + } + else + { + _s.EndWrite(asyncResult); + } + } public override void Flush() { if (IsRestrictionEnabled) diff --git a/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs b/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs index 5d610bb461cb4b..bf00c403244af9 100644 --- a/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs +++ b/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs @@ -322,7 +322,7 @@ private static string FlipSlashes(string name) public static FileStream CreateFileStreamRead(bool async, string fileName) => new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: async); - public static void DirsEqual(string actual, string expected) + public static async Task DirsEqual(string actual, string expected) { var expectedList = FileData.InPath(expected); var actualList = Directory.GetFiles(actual, "*.*", SearchOption.AllDirectories); @@ -330,8 +330,8 @@ public static void DirsEqual(string actual, string expected) var actualCount = actualList.Length + actualFolders.Length; Assert.Equal(expectedList.Count, actualCount); - ItemEqual(actualList, expectedList, isFile: true); - ItemEqual(actualFolders, expectedList, isFile: false); + await ItemEqual(actualList, expectedList, isFile: true); + await ItemEqual(actualFolders, expectedList, isFile: false); } public static void DirFileNamesEqual(string actual, string expected) @@ -341,7 +341,7 @@ public static void DirFileNamesEqual(string actual, string expected) AssertExtensions.SequenceEqual(expectedEntries.Select(Path.GetFileName).ToArray(), actualEntries.Select(Path.GetFileName).ToArray()); } - private static void ItemEqual(string[] actualList, List expectedList, bool isFile) + private static async Task ItemEqual(string[] actualList, List expectedList, bool isFile) { for (int i = 0; i < actualList.Length; i++) { @@ -362,8 +362,8 @@ private static void ItemEqual(string[] actualList, List expectedList, //contents same if (isFile) { - Stream sa = StreamHelpers.CreateTempCopyStream(aEntry).Result; - Stream sb = StreamHelpers.CreateTempCopyStream(bEntry).Result; + Stream sa = await StreamHelpers.CreateTempCopyStream(aEntry); + Stream sb = await StreamHelpers.CreateTempCopyStream(bEntry); StreamsEqual(sa, sb); // Not testing zip features, can always be async } } @@ -597,9 +597,9 @@ public static IEnumerable Utf8Comment_Data() } foreach (object[] e in SharedComment_Data()) - { - yield return e; - } + { + yield return e; + } } // Returns pairs as expected by Latin1 @@ -617,9 +617,9 @@ public static IEnumerable Latin1Comment_Data() } foreach (object[] e in SharedComment_Data()) - { - yield return e; - } + { + yield return e; + } } // Returns pairs encoded with Latin1, but decoded with UTF8. diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs index 32334bd386a3f4..06062c24c945af 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs @@ -8,7 +8,6 @@ namespace System.IO.Compression.Tests; -[SkipOnPlatform(TestPlatforms.Browser, "https://github.com/dotnet/runtime/issues/114769")] public class ZipFile_Extract_Stream : ZipFileTestBase { [Fact] @@ -47,7 +46,7 @@ public async Task ExtractToDirectoryNormal(string file, string folder, bool asyn string folderName = zfolder(folder); using TempDirectory tempFolder = new(GetTestFilePath()); await CallZipFileExtractToDirectory(async, source, tempFolder.Path); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); await DisposeStream(async, source); } @@ -74,7 +73,7 @@ public async Task ExtractToDirectoryNormal_Unwritable_Unseekable(string file, st string folderName = zfolder(folder); using TempDirectory tempFolder = new(GetTestFilePath()); await CallZipFileExtractToDirectory(async, source, tempFolder.Path); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); await DisposeStream(async, fs); } @@ -200,7 +199,7 @@ public async Task ExtractToDirectoryOverwrite(bool async) source.Position = 0; await CallZipFileExtractToDirectory(async, source, tempFolder.Path, overwriteFiles: true); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); await DisposeStream(async, source); } @@ -222,7 +221,7 @@ public async Task ExtractToDirectoryOverwriteEncoding(bool async) source.Position = 0; await CallZipFileExtractToDirectory(async, source, tempFolder.Path, Encoding.UTF8, overwriteFiles: true); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); } [Theory] diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs index 22b4be18b8c5d4..bcf2eecdb58d74 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs @@ -8,7 +8,6 @@ namespace System.IO.Compression.Tests { - [SkipOnPlatform(TestPlatforms.Browser, "https://github.com/dotnet/runtime/issues/114769")] public class ZipFile_Extract : ZipFileTestBase { public static IEnumerable Get_ExtractToDirectoryNormal_Data() @@ -33,7 +32,7 @@ public async Task ExtractToDirectoryNormal(string file, string folder, bool asyn string folderName = zfolder(folder); using TempDirectory tempFolder = new TempDirectory(GetTestFilePath()); await CallZipFileExtractToDirectory(async, zipFileName, tempFolder.Path); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); } [Theory] @@ -157,7 +156,7 @@ public async Task ExtractToDirectoryOverwrite(bool async) await Assert.ThrowsAsync(() => CallZipFileExtractToDirectory(async, zipFileName, tempFolder.Path, overwriteFiles: false)); await CallZipFileExtractToDirectory(async, zipFileName, tempFolder.Path, overwriteFiles: true); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); } [Theory] @@ -174,7 +173,7 @@ public async Task ExtractToDirectoryOverwriteEncoding(bool async) await Assert.ThrowsAsync(() => CallZipFileExtractToDirectory(async, zipFileName, tempFolder.Path, Encoding.UTF8, overwriteFiles: false)); await CallZipFileExtractToDirectory(async, zipFileName, tempFolder.Path, Encoding.UTF8, overwriteFiles: true); - DirsEqual(tempFolder.Path, folderName); + await DirsEqual(tempFolder.Path, folderName); } [Theory] diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFileExtensions.ZipArchive.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFileExtensions.ZipArchive.Extract.cs index b67071f1c15061..46c4c2a08a4cbc 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFileExtensions.ZipArchive.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFileExtensions.ZipArchive.Extract.cs @@ -19,7 +19,7 @@ public async Task ExtractToDirectoryExtension(bool async) await Assert.ThrowsAsync(() => CallZipFileExtensionsExtractToDirectory(async, archive, null)); await CallZipFileExtensionsExtractToDirectory(async, archive, tempFolder); - DirsEqual(tempFolder, zfolder("normal")); + await DirsEqual(tempFolder, zfolder("normal")); await DisposeZipArchive(async, archive); } diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs index 384168bd8eeb51..215d8f6249aa92 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -281,10 +281,9 @@ public static async Task ZipArchiveEntry_CorruptedStream_ReadMode_Read_UpToUncom [Theory] [MemberData(nameof(Get_Booleans_Data))] - [SkipOnPlatform(TestPlatforms.Browser, "https://github.com/dotnet/runtime/issues/114769")] public static async Task ZipArchiveEntry_CorruptedStream_EnsureNoExtraBytesReadOrOverWritten(bool async) { - MemoryStream stream = PopulateStream().Result; + MemoryStream stream = await LocalMemoryStream.ReadAppFileAsync(zfile("normal.zip")); int nameOffset = PatchDataRelativeToFileName(Encoding.ASCII.GetBytes(s_tamperedFileName), stream, 8); // patch uncompressed size in file header PatchDataRelativeToFileName(Encoding.ASCII.GetBytes(s_tamperedFileName), stream, 22, nameOffset + s_tamperedFileName.Length); // patch in central directory too @@ -315,11 +314,6 @@ public static async Task ZipArchiveEntry_CorruptedStream_EnsureNoExtraBytesReadO await DisposeZipArchive(async, archive); } - private static async Task PopulateStream() - { - return await LocalMemoryStream.ReadAppFileAsync(zfile("normal.zip")); - } - [Theory] [MemberData(nameof(Get_Booleans_Data))] public static async Task Zip64ArchiveEntry_CorruptedStream_CopyTo_UpToUncompressedSize(bool async) diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs index f36b8653ecc334..7a8cdacec9daf2 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs @@ -314,7 +314,7 @@ public static async Task AddFileToArchive_ThenReadEntries(bool async) await IsZipSameAsDir(testArchive, zmodified("addFile"), ZipArchiveMode.Read, requireExplicit: true, checkTimes: true, async); } - private static Task CreateAndUpdateEntry(ZipArchive archive, string installFile, string entryName, bool async) + private static Task CreateAndUpdateEntry(ZipArchive archive, string installFile, string entryName, bool async) { ZipArchiveEntry e = archive.CreateEntry(entryName); return UpdateEntry(e, installFile, entryName, async); @@ -472,15 +472,20 @@ public async Task Update_PerformMinimalWritesWhenNoFilesChanged(bool async) } } + public static IEnumerable Get_Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged_Data() + { + yield return [ 49, 1, 1, ]; + yield return [ 40, 3, 2, ]; + yield return [ 30, 5, 3, ]; + yield return [ 0, 8, 1, ]; + } + [Theory] - [InlineData(49, 1, 1)] - [InlineData(40, 3, 2)] - [InlineData(30, 5, 3)] - [InlineData(0, 8, 1)] - public void Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged(int startIndex, int entriesToModify, int step) + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged_Data))] + public async Task Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged(int startIndex, int entriesToModify, int step) { byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - byte[] sampleZipFile = CreateZipFile(50, sampleEntryContents, async: false).Result; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); using (MemoryStream ms = new MemoryStream()) { @@ -554,113 +559,115 @@ public void Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged(in } } - //[Theory] - //[InlineData(49, 1, 1)] - //[InlineData(40, 3, 2)] - //[InlineData(30, 5, 3)] - //[InlineData(0, 8, 1)] - //public async Task Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged_Async(int startIndex, int entriesToModify, int step) - //{ - // byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - // byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: true); - - // await using (MemoryStream ms = new MemoryStream()) - // { - // await ms.WriteAsync(sampleZipFile); - // ms.Seek(0, SeekOrigin.Begin); - - // await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) - // { - // // Open the first archive in Update mode, then change the value of {entriesToModify} fixed-length entry headers - // // (LastWriteTime.) Verify the correct number of writes performed as a result, then reopen the same - // // archive, get the entries and make sure that the fields hold the expected value. - // int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.Write)); - // int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); - // ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); - // List<(string EntryName, DateTimeOffset LastWriteTime)> updatedMetadata = new(entriesToModify); - - // for (int i = 0; i < entriesToModify; i++) - // { - // int modificationIndex = startIndex + (i * step); - // ZipArchiveEntry entryToModify = target.Entries[modificationIndex]; - // string entryName = entryToModify.FullName; - // DateTimeOffset expectedDateTimeOffset = entryToModify.LastWriteTime.AddHours(1.0); - - // entryToModify.LastWriteTime = expectedDateTimeOffset; - // updatedMetadata.Add((entryName, expectedDateTimeOffset)); - // } - - // await target.DisposeAsync(); - - // writesCalled = trackingStream.TimesCalled(nameof(trackingStream.Write)) - writesCalled; - // writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; - // // As above, check 1: the number of writes performed should be minimal. - // // 2 writes per archive entry for the local file header. - // // 2 writes per archive entry for the central directory header. - // // 1 write (sometimes 2, if there's a comment) for the end of central directory block. - // // The EOCD block won't change as a result of our modifications, so is excluded from the counts. - // Assert.Equal(((2 + 2) * entriesToModify), writesCalled + writeBytesCalled); - - // trackingStream.Seek(0, SeekOrigin.Begin); - // target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null); - - // for (int i = 0; i < entriesToModify; i++) - // { - // int modificationIndex = startIndex + (i * step); - // var expectedValues = updatedMetadata[i]; - // ZipArchiveEntry verifiedEntry = target.Entries[modificationIndex]; - - // // Check 2: the field holds the expected value (and thus has been written to the file.) - // Assert.NotNull(verifiedEntry); - // Assert.Equal(expectedValues.EntryName, verifiedEntry.FullName); - // Assert.Equal(expectedValues.LastWriteTime, verifiedEntry.LastWriteTime); - // } - - // // Check 3: no other data has been corrupted as a result - // for (int i = 0; i < target.Entries.Count; i++) - // { - // ZipArchiveEntry entry = target.Entries[i]; - // byte[] expectedBuffer = [.. sampleEntryContents, (byte)(i % byte.MaxValue)]; - // byte[] readBuffer = new byte[expectedBuffer.Length]; - - // await using (Stream readStream = await entry.OpenAsync()) - // { - // await readStream.ReadAsync(readBuffer); - // } - - // Assert.Equal(expectedBuffer, readBuffer); - // } - - // await target.DisposeAsync(); - // } - // } - //} + [Theory] + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged_Data))] + public async Task Update_PerformMinimalWritesWhenFixedLengthEntryHeaderFieldChanged_Async(int startIndex, int entriesToModify, int step) + { + byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); + + await using (MemoryStream ms = new MemoryStream()) + { + await ms.WriteAsync(sampleZipFile); + ms.Seek(0, SeekOrigin.Begin); + + await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) + { + // Open the first archive in Update mode, then change the value of {entriesToModify} fixed-length entry headers + // (LastWriteTime.) Verify the correct number of writes performed as a result, then reopen the same + // archive, get the entries and make sure that the fields hold the expected value. + int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)); + int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); + ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); + List<(string EntryName, DateTimeOffset LastWriteTime)> updatedMetadata = new(entriesToModify); + + for (int i = 0; i < entriesToModify; i++) + { + int modificationIndex = startIndex + (i * step); + ZipArchiveEntry entryToModify = target.Entries[modificationIndex]; + string entryName = entryToModify.FullName; + DateTimeOffset expectedDateTimeOffset = entryToModify.LastWriteTime.AddHours(1.0); + + entryToModify.LastWriteTime = expectedDateTimeOffset; + updatedMetadata.Add((entryName, expectedDateTimeOffset)); + } + + await target.DisposeAsync(); + + writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)) - writesCalled; + writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; + // As above, check 1: the number of writes performed should be minimal. + // 2 writes per archive entry for the local file header. + // 2 writes per archive entry for the central directory header. + // 1 write (sometimes 2, if there's a comment) for the end of central directory block. + // The EOCD block won't change as a result of our modifications, so is excluded from the counts. + Assert.Equal(((2 + 2) * entriesToModify), writesCalled + writeBytesCalled); + + trackingStream.Seek(0, SeekOrigin.Begin); + target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null); + + for (int i = 0; i < entriesToModify; i++) + { + int modificationIndex = startIndex + (i * step); + var expectedValues = updatedMetadata[i]; + ZipArchiveEntry verifiedEntry = target.Entries[modificationIndex]; + + // Check 2: the field holds the expected value (and thus has been written to the file.) + Assert.NotNull(verifiedEntry); + Assert.Equal(expectedValues.EntryName, verifiedEntry.FullName); + Assert.Equal(expectedValues.LastWriteTime, verifiedEntry.LastWriteTime); + } + + // Check 3: no other data has been corrupted as a result + for (int i = 0; i < target.Entries.Count; i++) + { + ZipArchiveEntry entry = target.Entries[i]; + byte[] expectedBuffer = [.. sampleEntryContents, (byte)(i % byte.MaxValue)]; + byte[] readBuffer = new byte[expectedBuffer.Length]; + + await using (Stream readStream = await entry.OpenAsync()) + { + await readStream.ReadAsync(readBuffer.AsMemory()); + } + + Assert.Equal(expectedBuffer, readBuffer); + } + + await target.DisposeAsync(); + } + } + } + + public static IEnumerable Get_Update_PerformMinimalWritesWhenEntryDataChanges_Data() + { + yield return new object[] { 0, }; + yield return new object[] { 10, }; + yield return new object[] { 20, }; + yield return new object[] { 30, }; + yield return new object[] { 49, }; + } + + [Theory] + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenEntryDataChanges_Data))] + public Task Update_PerformMinimalWritesWhenEntryDataChanges(int index) => Update_PerformMinimalWritesWithDataAndHeaderChanges(index, -1); [Theory] - [InlineData(0)] - [InlineData(10)] - [InlineData(20)] - [InlineData(30)] - [InlineData(49)] - public void Update_PerformMinimalWritesWhenEntryDataChanges(int index) - => Update_PerformMinimalWritesWithDataAndHeaderChanges(index, -1); - - //[Theory] - //[InlineData(0)] - //[InlineData(10)] - //[InlineData(20)] - //[InlineData(30)] - //[InlineData(49)] - //public Task Update_PerformMinimalWritesWhenEntryDataChanges_Async(int index) => Update_PerformMinimalWritesWithDataAndHeaderChanges_Async(index, -1); + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenEntryDataChanges_Data))] + public Task Update_PerformMinimalWritesWhenEntryDataChanges_Async(int index) => Update_PerformMinimalWritesWithDataAndHeaderChanges_Async(index, -1); + + public static IEnumerable Get_PerformMinimalWritesWithDataAndHeaderChanges_Data() + { + yield return [ 0, 0 ]; + yield return [ 20, 40 ]; + yield return [ 30, 10 ]; + } [Theory] - [InlineData(0, 0)] - [InlineData(20, 40)] - [InlineData(30, 10)] - public void Update_PerformMinimalWritesWithDataAndHeaderChanges(int dataChangeIndex, int lastWriteTimeChangeIndex) + [MemberData(nameof(Get_PerformMinimalWritesWithDataAndHeaderChanges_Data))] + public async Task Update_PerformMinimalWritesWithDataAndHeaderChanges(int dataChangeIndex, int lastWriteTimeChangeIndex) { byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - byte[] sampleZipFile = CreateZipFile(50, sampleEntryContents, async: false).Result; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); byte[] expectedUpdatedEntryContents = [19, 18, 17, 16, 15, 14, 13, 12, 11, 10]; using (MemoryStream ms = new MemoryStream()) @@ -751,105 +758,103 @@ public void Update_PerformMinimalWritesWithDataAndHeaderChanges(int dataChangeIn } } - //[Theory] - //[InlineData(0, 0)] - //[InlineData(20, 40)] - //[InlineData(30, 10)] - //public async Task Update_PerformMinimalWritesWithDataAndHeaderChanges_Async(int dataChangeIndex, int lastWriteTimeChangeIndex) - //{ - // byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - // byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: true); - // byte[] expectedUpdatedEntryContents = [19, 18, 17, 16, 15, 14, 13, 12, 11, 10]; - - // await using (MemoryStream ms = new MemoryStream()) - // { - // await ms.WriteAsync(sampleZipFile); - // ms.Seek(0, SeekOrigin.Begin); - - // await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) - // { - // // Open the archive in Update mode, then rewrite the data of the {dataChangeIndex}th entry - // // and set the LastWriteTime of the {lastWriteTimeChangeIndex}th entry. - // // Verify the correct number of writes performed as a result, then reopen the same - // // archive, get the entries and make sure that the fields hold the expected value. - // int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.Write)); - // int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); - // ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); - // ZipArchiveEntry entryToRewrite = target.Entries[dataChangeIndex]; - // int totalEntries = target.Entries.Count; - // int expectedEntriesToWrite = target.Entries.Count - dataChangeIndex; - // DateTimeOffset expectedWriteTime = default; - - // if (lastWriteTimeChangeIndex != -1) - // { - // ZipArchiveEntry entryToModify = target.Entries[lastWriteTimeChangeIndex]; - - // expectedWriteTime = entryToModify.LastWriteTime.AddHours(1.0); - // entryToModify.LastWriteTime = expectedWriteTime; - // } - - // await using (var entryStream = await entryToRewrite.OpenAsync()) - // { - // entryStream.SetLength(0); - // await entryStream.WriteAsync(expectedUpdatedEntryContents); - // } - - // await target.DisposeAsync(); - - // writesCalled = trackingStream.TimesCalled(nameof(trackingStream.Write)) - writesCalled; - // writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; - - // // If the data changed first, then every entry after it will be written in full. If the fixed-length - // // metadata changed first, some entries which won't have been fully written - just updated in place. - // // 2 writes per archive entry for the local file header. - // // 2 writes per archive entry for the central directory header. - // // 2 writes for the file data of the updated entry itself - // // 1 write per archive entry for the file data of other entries after this in the file - // // 1 write (sometimes 2, if there's a comment) for the end of central directory block. - // // All of the central directory headers must be rewritten after an entry's data has been modified. - // if (dataChangeIndex <= lastWriteTimeChangeIndex || lastWriteTimeChangeIndex == -1) - // { - // // dataChangeIndex -> totalEntries: rewrite in full - // // all central directories headers - // Assert.Equal(1 + 1 + ((2 + 1) * expectedEntriesToWrite) + (2 * totalEntries), writesCalled + writeBytesCalled); - // } - // else - // { - // // lastWriteTimeChangeIndex: partial rewrite - // // dataChangeIndex -> totalEntries: rewrite in full - // // all central directory headers - // Assert.Equal(1 + 1 + ((2 + 1) * expectedEntriesToWrite) + (2 * totalEntries) + 2, writesCalled + writeBytesCalled); - // } - - // trackingStream.Seek(0, SeekOrigin.Begin); - // target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null); - - // // Check 2: no other data has been corrupted as a result - // for (int i = 0; i < target.Entries.Count; i++) - // { - // ZipArchiveEntry entry = target.Entries[i]; - // byte[] expectedBuffer = i == dataChangeIndex - // ? expectedUpdatedEntryContents - // : [.. sampleEntryContents, (byte)(i % byte.MaxValue)]; - // byte[] readBuffer = new byte[expectedBuffer.Length]; - - // await using (Stream readStream = await entry.OpenAsync()) - // { - // await readStream.ReadAsync(readBuffer); - // } - - // Assert.Equal(expectedBuffer, readBuffer); - - // if (i == lastWriteTimeChangeIndex) - // { - // Assert.Equal(expectedWriteTime, entry.LastWriteTime); - // } - // } - - // await target.DisposeAsync(); - // } - // } - //} + [Theory] + [MemberData(nameof(Get_PerformMinimalWritesWithDataAndHeaderChanges_Data))] + public async Task Update_PerformMinimalWritesWithDataAndHeaderChanges_Async(int dataChangeIndex, int lastWriteTimeChangeIndex) + { + byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: true); + byte[] expectedUpdatedEntryContents = [19, 18, 17, 16, 15, 14, 13, 12, 11, 10]; + + await using (MemoryStream ms = new MemoryStream()) + { + await ms.WriteAsync(sampleZipFile); + ms.Seek(0, SeekOrigin.Begin); + + await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) + { + // Open the archive in Update mode, then rewrite the data of the {dataChangeIndex}th entry + // and set the LastWriteTime of the {lastWriteTimeChangeIndex}th entry. + // Verify the correct number of writes performed as a result, then reopen the same + // archive, get the entries and make sure that the fields hold the expected value. + int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)); + int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); + ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); + ZipArchiveEntry entryToRewrite = target.Entries[dataChangeIndex]; + int totalEntries = target.Entries.Count; + int expectedEntriesToWrite = target.Entries.Count - dataChangeIndex; + DateTimeOffset expectedWriteTime = default; + + if (lastWriteTimeChangeIndex != -1) + { + ZipArchiveEntry entryToModify = target.Entries[lastWriteTimeChangeIndex]; + + expectedWriteTime = entryToModify.LastWriteTime.AddHours(1.0); + entryToModify.LastWriteTime = expectedWriteTime; + } + + await using (var entryStream = await entryToRewrite.OpenAsync()) + { + entryStream.SetLength(0); + await entryStream.WriteAsync(expectedUpdatedEntryContents); + } + + await target.DisposeAsync(); + + writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)) - writesCalled; + writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; + + // If the data changed first, then every entry after it will be written in full. If the fixed-length + // metadata changed first, some entries which won't have been fully written - just updated in place. + // 2 writes per archive entry for the local file header. + // 2 writes per archive entry for the central directory header. + // 2 writes for the file data of the updated entry itself + // 1 write per archive entry for the file data of other entries after this in the file + // 1 write (sometimes 2, if there's a comment) for the end of central directory block. + // All of the central directory headers must be rewritten after an entry's data has been modified. + if (dataChangeIndex <= lastWriteTimeChangeIndex || lastWriteTimeChangeIndex == -1) + { + // dataChangeIndex -> totalEntries: rewrite in full + // all central directories headers + Assert.Equal(1 + 1 + ((2 + 1) * expectedEntriesToWrite) + (2 * totalEntries), writesCalled + writeBytesCalled); + } + else + { + // lastWriteTimeChangeIndex: partial rewrite + // dataChangeIndex -> totalEntries: rewrite in full + // all central directory headers + Assert.Equal(1 + 1 + ((2 + 1) * expectedEntriesToWrite) + (2 * totalEntries) + 2, writesCalled + writeBytesCalled); + } + + trackingStream.Seek(0, SeekOrigin.Begin); + target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null); + + // Check 2: no other data has been corrupted as a result + for (int i = 0; i < target.Entries.Count; i++) + { + ZipArchiveEntry entry = target.Entries[i]; + byte[] expectedBuffer = i == dataChangeIndex + ? expectedUpdatedEntryContents + : [.. sampleEntryContents, (byte)(i % byte.MaxValue)]; + byte[] readBuffer = new byte[expectedBuffer.Length]; + + await using (Stream readStream = await entry.OpenAsync()) + { + await readStream.ReadAsync(readBuffer); + } + + Assert.Equal(expectedBuffer, readBuffer); + + if (i == lastWriteTimeChangeIndex) + { + Assert.Equal(expectedWriteTime, entry.LastWriteTime); + } + } + + await target.DisposeAsync(); + } + } + } [Fact] public async Task Update_PerformMinimalWritesWhenArchiveCommentChanged() @@ -875,19 +880,53 @@ public async Task Update_PerformMinimalWritesWhenArchiveCommentChanged() target = new ZipArchive(trackingStream, ZipArchiveMode.Read, leaveOpen: true); Assert.Equal(expectedComment, target.Comment); + target.Dispose(); + } + } + + [Fact] + public async Task Update_PerformMinimalWritesWhenArchiveCommentChanged_Async() + { + await using (LocalMemoryStream ms = await LocalMemoryStream.ReadAppFileAsync(zfile("normal.zip"))) + await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) + { + int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)); + int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); + string expectedComment = "14 byte comment"; + + ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); + target.Comment = expectedComment; + await target.DisposeAsync(); + + writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)) - writesCalled; + writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; + + // We expect 2 writes for the end of central directory block - 1 for the EOCD, 1 for the comment. + Assert.Equal(2, writesCalled + writeBytesCalled); + + trackingStream.Seek(0, SeekOrigin.Begin); + + target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null); + Assert.Equal(expectedComment, target.Comment); + await target.DisposeAsync(); } } + public static IEnumerable Get_Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted_Data() + { + yield return [ -1, 40 ]; + yield return [ -1, 49 ]; + yield return [ -1, 0 ]; + yield return [ 42, 40 ]; + yield return [ 38, 40 ]; + } + [Theory] - [InlineData(-1, 40)] - [InlineData(-1, 49)] - [InlineData(-1, 0)] - [InlineData(42, 40)] - [InlineData(38, 40)] - public void Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted(int modifyIndex, int deleteIndex) + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted_Data))] + public async Task Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted(int modifyIndex, int deleteIndex) { byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - byte[] sampleZipFile = CreateZipFile(50, sampleEntryContents, async: false).Result; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); byte[] expectedUpdatedEntryContents = [22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; using (MemoryStream ms = new MemoryStream()) @@ -976,14 +1015,112 @@ public void Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted(int modifyI } [Theory] - [InlineData(1)] - [InlineData(5)] - [InlineData(10)] - [InlineData(12)] - public void Update_PerformMinimalWritesWhenEntriesModifiedAndAdded(int entriesToCreate) + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted_Data))] + public async Task Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted_Async(int modifyIndex, int deleteIndex) { byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; - byte[] sampleZipFile = CreateZipFile(50, sampleEntryContents, async: false).Result; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); + byte[] expectedUpdatedEntryContents = [22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; + + await using (MemoryStream ms = new MemoryStream()) + { + await ms.WriteAsync(sampleZipFile); + ms.Seek(0, SeekOrigin.Begin); + + await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) + { + // Open the archive in Update mode, then rewrite the data of the {modifyIndex}th entry + // and delete the LastWriteTime of the {lastWriteTimeChangeIndex}th entry. + // Verify the correct number of writes performed as a result, then reopen the same + // archive, get the entries, make sure that the right number of entries have been + // found and that the entries have the correct contents. + int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)); + int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); + ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); + int totalEntries = target.Entries.Count; + // Everything after the first modification or deletion is to be rewritten. + int expectedEntriesToWrite = (totalEntries - 1) - (modifyIndex == -1 ? deleteIndex : Math.Min(modifyIndex, deleteIndex)); + ZipArchiveEntry entryToDelete = target.Entries[deleteIndex]; + string deletedPath = entryToDelete.FullName; + string modifiedPath = null; + + if (modifyIndex != -1) + { + ZipArchiveEntry entryToRewrite = target.Entries[modifyIndex]; + + modifiedPath = entryToRewrite.FullName; + await using (var entryStream = await entryToRewrite.OpenAsync()) + { + entryStream.SetLength(0); + await entryStream.WriteAsync(expectedUpdatedEntryContents); + } + } + + entryToDelete.Delete(); + + await target.DisposeAsync(); + + Assert.True(ms.Length < sampleZipFile.Length); + + writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)) - writesCalled; + writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; + + // 2 writes per archive entry for the local file header. + // 2 writes per archive entry for the central directory header. + // 2 writes for the file data of the updated entry itself + // 1 write per archive entry for the file data of other entries after this in the file + // 1 write (sometimes 2, if there's a comment) for the end of central directory block. + // All of the central directory headers must be rewritten after an entry's data has been modified. + if (modifyIndex == -1) + { + Assert.Equal(1 + ((2 + 1) * expectedEntriesToWrite) + (2 * (totalEntries - 1)), writesCalled + writeBytesCalled); + } + else + { + Assert.Equal(1 + 1 + ((2 + 1) * expectedEntriesToWrite) + (2 * (totalEntries - 1)), writesCalled + writeBytesCalled); + } + + trackingStream.Seek(0, SeekOrigin.Begin); + target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null); + + // Check 2: no other data has been corrupted as a result + for (int i = 0; i < target.Entries.Count; i++) + { + ZipArchiveEntry entry = target.Entries[i]; + // The expected index will be off by one if it's after the deleted index, so compensate + int expectedIndex = i < deleteIndex ? i : i + 1; + byte[] expectedBuffer = entry.FullName == modifiedPath + ? expectedUpdatedEntryContents + : [.. sampleEntryContents, (byte)(expectedIndex % byte.MaxValue)]; + byte[] readBuffer = new byte[expectedBuffer.Length]; + + await using (Stream readStream = await entry.OpenAsync()) + { + await readStream.ReadAsync(readBuffer.AsMemory()); + } + + Assert.Equal(expectedBuffer, readBuffer); + + Assert.NotEqual(deletedPath, entry.FullName); + } + } + } + } + + public static IEnumerable Get_Update_PerformMinimalWritesWhenEntriesModifiedAndAdded_Data() + { + yield return [ 1 ]; + yield return [ 5 ]; + yield return [ 10 ]; + yield return [ 12 ]; + } + + [Theory] + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenEntriesModifiedAndAdded_Data))] + public async Task Update_PerformMinimalWritesWhenEntriesModifiedAndAdded(int entriesToCreate) + { + byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); using (MemoryStream ms = new MemoryStream()) { @@ -1058,5 +1195,87 @@ public void Update_PerformMinimalWritesWhenEntriesModifiedAndAdded(int entriesTo } } } + + + [Theory] + [MemberData(nameof(Get_Update_PerformMinimalWritesWhenEntriesModifiedAndAdded_Data))] + public async Task Update_PerformMinimalWritesWhenEntriesModifiedAndAdded_Async(int entriesToCreate) + { + byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + byte[] sampleZipFile = await CreateZipFile(50, sampleEntryContents, async: false); + + await using (MemoryStream ms = new MemoryStream()) + { + await ms.WriteAsync(sampleZipFile); + ms.Seek(0, SeekOrigin.Begin); + + await using (CallTrackingStream trackingStream = new CallTrackingStream(ms)) + { + // Open the archive in Update mode. Rewrite the data of the first entry and add five entries + // to the end of the archive. Verify the correct number of writes performed as a result, then + // reopen the same archive, get the entries, make sure that the right number of entries have + // been found and that the entries have the correct contents. + int writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)); + int writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)); + ZipArchive target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null); + int totalEntries = target.Entries.Count; + ZipArchiveEntry entryToRewrite = target.Entries[^1]; + string modifiedPath = entryToRewrite.FullName; + + await using (Stream entryStream = await entryToRewrite.OpenAsync()) + { + entryStream.Seek(0, SeekOrigin.Begin); + for (int i = 0; i < 100; i++) + { + await entryStream.WriteAsync(sampleEntryContents); + } + } + + for (int i = 0; i < entriesToCreate; i++) + { + ZipArchiveEntry createdEntry = target.CreateEntry($"added/{i}.bin"); + + using (Stream entryWriteStream = createdEntry.Open()) + { + await entryWriteStream.WriteAsync(sampleEntryContents); + entryWriteStream.WriteByte((byte)((i + totalEntries) % byte.MaxValue)); + } + } + + await target.DisposeAsync(); + + writesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteAsync)) - writesCalled; + writeBytesCalled = trackingStream.TimesCalled(nameof(trackingStream.WriteByte)) - writeBytesCalled; + + // 2 writes per archive entry for the local file header. + // 2 writes per archive entry for the central directory header. + // 2 writes for the file data of the updated entry itself + // 1 write (sometimes 2, if there's a comment) for the end of central directory block. + // All of the central directory headers must be rewritten after an entry's data has been modified. + + Assert.Equal(1 + ((2 + 2 + 2) * entriesToCreate) + (2 * (totalEntries - 1) + (2 + 2 + 2)), writesCalled + writeBytesCalled); + + trackingStream.Seek(0, SeekOrigin.Begin); + target = await ZipArchive.CreateAsync(trackingStream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null); + + // Check 2: no other data has been corrupted as a result + for (int i = 0; i < totalEntries + entriesToCreate; i++) + { + ZipArchiveEntry entry = target.Entries[i]; + byte[] expectedBuffer = entry.FullName == modifiedPath + ? Enumerable.Repeat(sampleEntryContents, 100).SelectMany(x => x).ToArray() + : [.. sampleEntryContents, (byte)(i % byte.MaxValue)]; + byte[] readBuffer = new byte[expectedBuffer.Length]; + + await using (Stream readStream = await entry.OpenAsync()) + { + await readStream.ReadAsync(readBuffer.AsMemory()); + } + + Assert.Equal(expectedBuffer, readBuffer); + } + } + } + } } }