Repository: dotnet/runtime
File: src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs (C#)

Code before:
                    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);
                    }
                }
            }
        }

        /// <summary>
        /// Tests that opening an entry stream and disposing it without writing does not mark the archive as modified,
        /// thus not triggering a rewrite on Dispose. This is the scenario from the bug where using a non-expandable
        /// MemoryStream with ZipArchiveMode.Update would throw NotSupportedException on Dispose if any entry was opened.
        /// </summary>
        [Theory]
        [MemberData(nameof(Get_Booleans_Data))]
        public async Task Update_OpenEntryWithoutWriting_DoesNotTriggerRewrite(bool async)
        {
            // Create a valid zip file
            byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
            byte[] sampleZipFile = await CreateZipFile(3, sampleEntryContents, async);
            long originalLength = sampleZipFile.Length;

            // Use a non-expandable MemoryStream (fixed buffer)
            // This would throw NotSupportedException if Dispose tries to write/grow the stream
            using (MemoryStream ms = new MemoryStream(sampleZipFile, writable: true))
            {
                ZipArchive archive = async
                    ? await ZipArchive.CreateAsync(ms, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null)
                    : new ZipArchive(ms, ZipArchiveMode.Update, leaveOpen: true);

                // Open an entry and read it without writing
                ZipArchiveEntry entry = archive.Entries[0];
                Stream entryStream = async ? await entry.OpenAsync() : entry.Open();
                byte[] buffer = new byte[sampleEntryContents.Length + 1]; // +1 for the index byte added by CreateZipFile
                int bytesRead = async
                    ? await entryStream.ReadAsync(buffer)
                    : entryStream.Read(buffer, 0, buffer.Length);

Review comment (bug):
I don't think it is useful to document which bug prompted us to add the test

Code after:
                    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);
                    }
                }
            }
        }

        /// <summary>
        /// Tests that opening an entry stream and disposing it without writing does not mark the archive as modified,
        /// thus not triggering a rewrite on Dispose.
        /// </summary>
        [Theory]
        [MemberData(nameof(Get_Booleans_Data))]
        public async Task Update_OpenEntryWithoutWriting_DoesNotTriggerRewrite(bool async)
        {
            // Create a valid zip file
            byte[] sampleEntryContents = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
            byte[] sampleZipFile = await CreateZipFile(3, sampleEntryContents, async);
            long originalLength = sampleZipFile.Length;

            // Keep a copy of the original contents to verify no in-place rewrite occurred
            byte[] originalContents = new byte[sampleZipFile.Length];
            Array.Copy(sampleZipFile, originalContents, sampleZipFile.Length);

            // Use a non-expandable MemoryStream (fixed buffer)
            // This would throw NotSupportedException if Dispose tries to write/grow the stream
            using (MemoryStream ms = new MemoryStream(sampleZipFile, writable: true))
            {
                ZipArchive archive = async
                    ? await ZipArchive.CreateAsync(ms, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null)
                    : new ZipArchive(ms, ZipArchiveMode.Update, leaveOpen: true);

                // Open an entry and read it without writing
                ZipArchiveEntry entry = archive.Entries[0];
                Stream entryStream = async ? await entry.OpenAsync() : entry.Open();
                byte[] buffer = new byte[sampleEntryContents.Length + 1]; // +1 for the index byte added by CreateZipFile