diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs index 67fbbd4ba400d8..410ea492dc7600 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs @@ -22,7 +22,6 @@ public partial class DeflateStream : Stream private Deflater? _deflater; private byte[]? _buffer; private volatile bool _activeAsyncOperation; - private bool _wroteBytes; internal DeflateStream(Stream stream, CompressionMode mode, long uncompressedSize) : this(stream, mode, leaveOpen: false, ZLibNative.Deflate_DefaultWindowBits, uncompressedSize) { @@ -62,7 +61,7 @@ internal DeflateStream(Stream stream, ZLibCompressionOptions compressionOptions, ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(compressionOptions); - InitializeDeflater(stream, (ZLibNative.CompressionLevel)compressionOptions.CompressionLevel, (CompressionStrategy)compressionOptions.CompressionStrategy, leaveOpen, windowBits); + InitializeDeflater(stream, (ZLibNative.CompressionLevel)compressionOptions.CompressionLevel, (CompressionStrategy)compressionOptions.CompressionStrategy, leaveOpen, windowBits); } /// @@ -558,7 +557,6 @@ internal void WriteCore(ReadOnlySpan buffer) { _deflater.SetInput(bufferPtr, buffer.Length); WriteDeflaterOutput(); - _wroteBytes = true; } } } @@ -579,25 +577,22 @@ private void WriteDeflaterOutput() // This is called by Flush: private void FlushBuffers() { - if (_wroteBytes) - { - // Compress any bytes left: - WriteDeflaterOutput(); + // Compress any bytes left: + WriteDeflaterOutput(); - Debug.Assert(_deflater != null && _buffer != null); - // Pull out any bytes left inside deflater: - bool flushSuccessful; - do + Debug.Assert(_deflater != null && _buffer != null); + // Pull out any bytes left inside deflater: + bool flushSuccessful; + do + { + int compressedBytes; + flushSuccessful = _deflater.Flush(_buffer, out compressedBytes); + if (flushSuccessful) { - int compressedBytes; - flushSuccessful = _deflater.Flush(_buffer, out compressedBytes); - if (flushSuccessful) - { - _stream.Write(_buffer, 0, compressedBytes); - } - Debug.Assert(flushSuccessful == (compressedBytes > 0)); - } while (flushSuccessful); - } + _stream.Write(_buffer, 0, compressedBytes); + } + Debug.Assert(flushSuccessful == (compressedBytes > 0)); + } while (flushSuccessful); // Always flush on the underlying stream _stream.Flush(); @@ -616,40 +611,19 @@ private void PurgeBuffers(bool disposing) return; Debug.Assert(_deflater != null && _buffer != null); - // Some deflaters (e.g. ZLib) write more than zero bytes for zero byte inputs. - // This round-trips and we should be ok with this, but our legacy managed deflater - // always wrote zero output for zero input and upstack code (e.g. ZipArchiveEntry) - // took dependencies on it. Thus, make sure to only "flush" when we actually had - // some input: - if (_wroteBytes) - { - // Compress any bytes left - WriteDeflaterOutput(); - - // Pull out any bytes left inside deflater: - bool finished; - do - { - int compressedBytes; - finished = _deflater.Finish(_buffer, out compressedBytes); + // Compress any bytes left + WriteDeflaterOutput(); - if (compressedBytes > 0) - _stream.Write(_buffer, 0, compressedBytes); - } while (!finished); - } - else + // Pull out any bytes left inside deflater: + bool finished; + do { - // In case of zero length buffer, we still need to clean up the native created stream before - // the object get disposed because eventually ZLibNative.ReleaseHandle will get called during - // the dispose operation and although it frees the stream but it return error code because the - // stream state was still marked as in use. The symptoms of this problem will not be seen except - // if running any diagnostic tools which check for disposing safe handle objects - bool finished; - do - { - finished = _deflater.Finish(_buffer, out _); - } while (!finished); - } + int compressedBytes; + finished = _deflater.Finish(_buffer, out compressedBytes); + + if (compressedBytes > 0) + _stream.Write(_buffer, 0, compressedBytes); + } while (!finished); } private async ValueTask PurgeBuffersAsync() @@ -663,40 +637,19 @@ private async ValueTask PurgeBuffersAsync() return; Debug.Assert(_deflater != null && _buffer != null); - // Some deflaters (e.g. ZLib) write more than zero bytes for zero byte inputs. - // This round-trips and we should be ok with this, but our legacy managed deflater - // always wrote zero output for zero input and upstack code (e.g. ZipArchiveEntry) - // took dependencies on it. Thus, make sure to only "flush" when we actually had - // some input. - if (_wroteBytes) - { - // Compress any bytes left - await WriteDeflaterOutputAsync(default).ConfigureAwait(false); - - // Pull out any bytes left inside deflater: - bool finished; - do - { - int compressedBytes; - finished = _deflater.Finish(_buffer, out compressedBytes); + // Compress any bytes left + await WriteDeflaterOutputAsync(default).ConfigureAwait(false); - if (compressedBytes > 0) - await _stream.WriteAsync(new ReadOnlyMemory(_buffer, 0, compressedBytes)).ConfigureAwait(false); - } while (!finished); - } - else + // Pull out any bytes left inside deflater: + bool finished; + do { - // In case of zero length buffer, we still need to clean up the native created stream before - // the object get disposed because eventually ZLibNative.ReleaseHandle will get called during - // the dispose operation and although it frees the stream, it returns an error code because the - // stream state was still marked as in use. The symptoms of this problem will not be seen except - // if running any diagnostic tools which check for disposing safe handle objects. - bool finished; - do - { - finished = _deflater.Finish(_buffer, out _); - } while (!finished); - } + int compressedBytes; + finished = _deflater.Finish(_buffer, out compressedBytes); + + if (compressedBytes > 0) + await _stream.WriteAsync(new ReadOnlyMemory(_buffer, 0, compressedBytes)).ConfigureAwait(false); + } while (!finished); } protected override void Dispose(bool disposing) @@ -850,8 +803,6 @@ async ValueTask Core(ReadOnlyMemory buffer, CancellationToken cancellation _deflater.SetInput(buffer); await WriteDeflaterOutputAsync(cancellationToken).ConfigureAwait(false); - - _wroteBytes = true; } finally { diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs index a7bdf84c4e23a7..c5289739e44685 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs @@ -702,30 +702,40 @@ private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, bool { // stream stack: backingStream -> DeflateStream -> CheckSumWriteStream - // By default we compress with deflate, except if compression level is set to NoCompression then stored is used. - // Stored is also used for empty files, but we don't actually call through this function for that - we just write the stored value in the header - // Deflate64 is not supported on all platforms + // By default we compress with deflate, except if compression level + // is set to NoCompression then stored is used. + // + // Stored is also used for empty files, but we can't know at this + // point if user will write anything to the stream or not. For that + // reason, we defer the instantiation of the compression stream + // until the first write to the CheckSumAndSizeWriteStream happens. + // If the user never writes anything, this will be detected during + // saving and the compression method in the file header will be + // changed to Stored. + // + // Note: Deflate64 is not supported on all platforms Debug.Assert(CompressionMethod == CompressionMethodValues.Deflate || CompressionMethod == CompressionMethodValues.Stored); + Func compressorStreamFactory; bool isIntermediateStream = true; - Stream compressorStream; + switch (CompressionMethod) { case CompressionMethodValues.Stored: - compressorStream = backingStream; + compressorStreamFactory = () => backingStream; isIntermediateStream = false; break; case CompressionMethodValues.Deflate: case CompressionMethodValues.Deflate64: default: - compressorStream = new DeflateStream(backingStream, _compressionLevel, leaveBackingStreamOpen); + compressorStreamFactory = () => new DeflateStream(backingStream, _compressionLevel, leaveBackingStreamOpen); break; } bool leaveCompressorStreamOpenOnClose = leaveBackingStreamOpen && !isIntermediateStream; var checkSumStream = new CheckSumAndSizeWriteStream( - compressorStream, + compressorStreamFactory, backingStream, leaveCompressorStreamOpenOnClose, this, @@ -975,7 +985,6 @@ private bool WriteLocalFileHeaderInitialize(bool isEmptyFile, bool forceWrite, o CompressionMethod = CompressionMethodValues.Stored; compressedSizeTruncated = 0; uncompressedSizeTruncated = 0; - Debug.Assert(_compressedSize == 0); Debug.Assert(_uncompressedSize == 0); Debug.Assert(_crc32 == 0); } diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs index b69f0b84c0827a..6f5e69cdab3ab7 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs @@ -402,7 +402,8 @@ protected override void Dispose(bool disposing) internal sealed class CheckSumAndSizeWriteStream : Stream { - private readonly Stream _baseStream; + private readonly Func _baseStreamFactory; + private Stream? _baseStream; private readonly Stream _baseBaseStream; private long _position; private uint _checksum; @@ -428,11 +429,11 @@ internal sealed class CheckSumAndSizeWriteStream : Stream // baseBaseStream it's a backingStream, passed here so as to avoid closure allocation, // zipArchiveEntry passed here so as to avoid closure allocation, // onClose handler passed here so as to avoid closure allocation - public CheckSumAndSizeWriteStream(Stream baseStream, Stream baseBaseStream, bool leaveOpenOnClose, + public CheckSumAndSizeWriteStream(Func baseStreamFactory, Stream baseBaseStream, bool leaveOpenOnClose, ZipArchiveEntry entry, EventHandler? onClose, Action saveCrcAndSizes) { - _baseStream = baseStream; + _baseStreamFactory = baseStreamFactory; _baseBaseStream = baseBaseStream; _position = 0; _checksum = 0; @@ -524,10 +525,15 @@ public override void Write(byte[] buffer, int offset, int count) if (!_everWritten) { + Debug.Assert(_baseStream == null); + _baseStream = _baseStreamFactory(); + _initialPosition = _baseBaseStream.Position; _everWritten = true; } + Debug.Assert(_baseStream != null); + _checksum = Crc32Helper.UpdateCrc32(_checksum, buffer, offset, count); _baseStream.Write(buffer, offset, count); _position += count; @@ -544,10 +550,15 @@ public override void Write(ReadOnlySpan source) if (!_everWritten) { + Debug.Assert(_baseStream == null); + _baseStream = _baseStreamFactory(); + _initialPosition = _baseBaseStream.Position; _everWritten = true; } + Debug.Assert(_baseStream != null); + _checksum = Crc32Helper.UpdateCrc32(_checksum, source); _baseStream.Write(source); _position += source.Length; @@ -575,10 +586,15 @@ async ValueTask Core(ReadOnlyMemory buffer, CancellationToken cancellation { if (!_everWritten) { + Debug.Assert(_baseStream == null); + _baseStream = _baseStreamFactory(); + _initialPosition = _baseBaseStream.Position; _everWritten = true; } + Debug.Assert(_baseStream != null); + _checksum = Crc32Helper.UpdateCrc32(_checksum, buffer.Span); await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); @@ -593,13 +609,13 @@ public override void Flush() // assume writable if not disposed Debug.Assert(CanWrite); - _baseStream.Flush(); + _baseStream?.Flush(); } public override Task FlushAsync(CancellationToken cancellationToken) { ThrowIfDisposed(); - return _baseStream.FlushAsync(cancellationToken); + return _baseStream?.FlushAsync(cancellationToken) ?? Task.CompletedTask; } protected override void Dispose(bool disposing) @@ -610,7 +626,7 @@ protected override void Dispose(bool disposing) if (!_everWritten) _initialPosition = _baseBaseStream.Position; if (!_leaveOpenOnClose) - _baseStream.Dispose(); // Close my super-stream (flushes the last data) + _baseStream?.Dispose(); // Close my super-stream (flushes the last data if we ever wrote any) _saveCrcAndSizes?.Invoke(_initialPosition, Position, _checksum, _baseBaseStream, _zipArchiveEntry, _onClose); _isDisposed = true; } @@ -624,8 +640,8 @@ public override async ValueTask DisposeAsync() // if we never wrote through here, save the position if (!_everWritten) _initialPosition = _baseBaseStream.Position; - if (!_leaveOpenOnClose) - await _baseStream.DisposeAsync().ConfigureAwait(false); // Close my super-stream (flushes the last data) + if (!_leaveOpenOnClose && _baseStream != null) + await _baseStream.DisposeAsync().ConfigureAwait(false); // Close my super-stream (flushes the last data if we ever wrote any) _saveCrcAndSizes?.Invoke(_initialPosition, Position, _checksum, _baseBaseStream, _zipArchiveEntry, _onClose); _isDisposed = true; } diff --git a/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs b/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs index 30ab788bab2d42..b2e2d48947f7bf 100644 --- a/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs +++ b/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs @@ -156,11 +156,11 @@ public void StreamTruncation_IsDetected(TestScenario testScenario) break; case TestScenario.Read: - while (ZipFileTestBase.ReadAllBytes(decompressor, buffer, 0, buffer.Length) != 0) { }; + while (ZipFileTestBase.ReadAllBytes(decompressor, buffer, 0, buffer.Length) != 0) { } break; case TestScenario.ReadAsync: - while (await ZipFileTestBase.ReadAllBytesAsync(decompressor, buffer, 0, buffer.Length) != 0) { }; + while (await ZipFileTestBase.ReadAllBytesAsync(decompressor, buffer, 0, buffer.Length) != 0) { } break; case TestScenario.ReadByte: @@ -219,5 +219,20 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati return base.WriteAsync(buffer, offset, count, cancellationToken); } } + + [Fact] + public void EmptyDeflateStream_WritesOutput() + { + using (var ms = new MemoryStream()) + { + using (var deflateStream = new DeflateStream(ms, CompressionMode.Compress, leaveOpen: true)) + { + // Write nothing + } + + // DeflateStream should now write output even for empty streams + Assert.True(ms.Length > 0, "Empty DeflateStream should write finalization data"); + } + } } } diff --git a/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Gzip.cs b/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Gzip.cs index 9919b2c819ae30..a0595950ffa7f8 100644 --- a/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Gzip.cs +++ b/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Gzip.cs @@ -126,7 +126,7 @@ public async Task ConcatenatedEmptyGzipStreams() [InlineData(10, TestScenario.ReadAsync, 1000, 2000)] [InlineData(10, TestScenario.Copy, 1000, 2000)] [InlineData(10, TestScenario.CopyAsync, 1000, 2000)] - [InlineData(2, TestScenario.Copy, 1000, 0x2000-30)] + [InlineData(2, TestScenario.Copy, 1000, 0x2000 - 30)] [InlineData(2, TestScenario.CopyAsync, 1000, 0x2000 - 30)] [InlineData(1000, TestScenario.Read, 1, 1)] [InlineData(1000, TestScenario.ReadAsync, 1, 1)] @@ -315,7 +315,7 @@ public void StreamCorruption_IsDetected() { Assert.Throws(() => { - while (ZipFileTestBase.ReadAllBytes(decompressor, buffer, 0, buffer.Length) != 0); + while (ZipFileTestBase.ReadAllBytes(decompressor, buffer, 0, buffer.Length) != 0) ; }); } } @@ -377,11 +377,11 @@ public void StreamTruncation_IsDetected(TestScenario testScenario) break; case TestScenario.Read: - while (ZipFileTestBase.ReadAllBytes(decompressor, buffer, 0, buffer.Length) != 0) { }; + while (ZipFileTestBase.ReadAllBytes(decompressor, buffer, 0, buffer.Length) != 0) { } break; case TestScenario.ReadAsync: - while (await ZipFileTestBase.ReadAllBytesAsync(decompressor, buffer, 0, buffer.Length) != 0) { }; + while (await ZipFileTestBase.ReadAllBytesAsync(decompressor, buffer, 0, buffer.Length) != 0) { } break; case TestScenario.ReadByte: @@ -441,5 +441,31 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati return base.WriteAsync(buffer, offset, count, cancellationToken); } } + + [Fact] + public void EmptyGZipStream_WritesHeaderAndFooter() + { + // Test that an empty GZip stream still writes the required headers and footers + using (var ms = new MemoryStream()) + { + using (var gzipStream = new GZipStream(ms, CompressionMode.Compress, leaveOpen: true)) + { + // Write nothing + } + + // At minimum it should have the GZip signature (0x1f 0x8b) and other required data + Assert.True(ms.Length > 0, "Empty GZip stream should write headers and footers"); + + // Verify the compressed data can be decompressed successfully + ms.Seek(0, SeekOrigin.Begin); + using (var decompressStream = new GZipStream(ms, CompressionMode.Decompress)) + using (var resultStream = new MemoryStream()) + { + decompressStream.CopyTo(resultStream); + Assert.Equal(0, resultStream.Length); + } + } + } + } } 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 215d8f6249aa92..5f178721284100 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -195,7 +195,7 @@ public static async Task LargeArchive_DataDescriptor_Read_NonZip64_FileLengthGre Stream source = await OpenEntryStream(async, e); byte[] buffer = new byte[s_bufferSize]; int read = await source.ReadAsync(buffer, 0, buffer.Length); // We don't want to inflate this large archive entirely - // just making sure it read successfully + // just making sure it read successfully Assert.Equal(s_bufferSize, read); foreach (byte b in buffer) { @@ -564,7 +564,7 @@ public static async Task UpdateZipArchive_AddFileTo_ZipWithCorruptedFile(bool as byte[] buffer2 = new byte[1024]; file.Seek(0, SeekOrigin.Begin); - while (await s.ReadAsync(buffer1, 0, buffer1.Length) != 0 ) + while (await s.ReadAsync(buffer1, 0, buffer1.Length) != 0) { await file.ReadAsync(buffer2, 0, buffer2.Length); Assert.Equal(buffer1, buffer2); @@ -820,7 +820,7 @@ public static IEnumerable EmptyFiles() [MemberData(nameof(EmptyFiles))] public async Task ReadArchive_WithEmptyDeflatedFile(byte[] fileBytes, bool async) { - using (var testStream = new MemoryStream(fileBytes)) + using (var testStream = new MemoryStream(fileBytes.ToArray())) { const string ExpectedFileName = "xl/customProperty2.bin"; @@ -834,7 +834,7 @@ public async Task ReadArchive_WithEmptyDeflatedFile(byte[] fileBytes, bool async byte[] fileContent = testStream.ToArray(); // compression method should not have changed - Assert.Equal(firstEntryCompressionMethod, fileBytes[8]); + Assert.Equal(firstEntryCompressionMethod, fileContent[8]); testStream.Seek(0, SeekOrigin.Begin); // second attempt: open archive with zero-length file that is compressed (Deflate = 0x8) @@ -1418,7 +1418,7 @@ public async Task ZipArchive_InvalidExtraFieldData_Async(byte validVersionToExtr // for first.bin would normally be skipped (because it hasn't changed) but it needs to be rewritten // because the central directory headers will be rewritten with a valid value and the local file header // needs to match. - await using (ZipArchive updatedArchive = await ZipArchive.CreateAsync(updatedStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null)) + await using (ZipArchive updatedArchive = await ZipArchive.CreateAsync(updatedStream, ZipArchiveMode.Update, leaveOpen: true, entryNameEncoding: null)) { ZipArchiveEntry newEntry = updatedArchive.CreateEntry("second.bin", CompressionLevel.NoCompression); 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 7a8cdacec9daf2..827e5f331124a1 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs @@ -92,62 +92,39 @@ public static async Task EmptyEntryTest(ZipArchiveMode mode, bool async) string data2 = "more test data written to file."; DateTimeOffset lastWrite = new DateTimeOffset(1992, 4, 5, 12, 00, 30, new TimeSpan(-5, 0, 0)); - var baseline = new LocalMemoryStream(); - ZipArchive archive = await CreateZipArchive(async, baseline, mode); - - await AddEntry(archive, "data1.txt", data1, lastWrite, async); - - ZipArchiveEntry e = archive.CreateEntry("empty.txt"); - e.LastWriteTime = lastWrite; - - Stream s = await OpenEntryStream(async, e); - await DisposeStream(async, s); - - await AddEntry(archive, "data2.txt", data2, lastWrite, async); - - await DisposeZipArchive(async, archive); - - var test = new LocalMemoryStream(); - archive = await CreateZipArchive(async, test, mode); - - await AddEntry(archive, "data1.txt", data1, lastWrite, async); - - e = archive.CreateEntry("empty.txt"); - e.LastWriteTime = lastWrite; - - await AddEntry(archive, "data2.txt", data2, lastWrite, async); - - await DisposeZipArchive(async, archive); - - //compare - Assert.True(ArraysEqual(baseline.ToArray(), test.ToArray()), "Arrays didn't match"); - - //second test, this time empty file at end - baseline = baseline.Clone(); - archive = await CreateZipArchive(async, baseline, mode); + async Task WriteTestArchive(bool openEntryStream, bool emptyEntryAtTheEnd) + { + var archiveStream = new LocalMemoryStream(); + ZipArchive archive = await CreateZipArchive(async, archiveStream, mode); - await AddEntry(archive, "data1.txt", data1, lastWrite, async); + await AddEntry(archive, "data1.txt", data1, lastWrite, async); - e = archive.CreateEntry("empty.txt"); - e.LastWriteTime = lastWrite; + ZipArchiveEntry e = archive.CreateEntry("empty.txt"); + e.LastWriteTime = lastWrite; - s = await OpenEntryStream(async, e); - await DisposeStream(async, s); - - await DisposeZipArchive(async, archive); + if (openEntryStream) + { + Stream s = await OpenEntryStream(async, e); + await DisposeStream(async, s); + } - test = test.Clone(); - archive = await CreateZipArchive(async, test, mode); + if (!emptyEntryAtTheEnd) + { + await AddEntry(archive, "data2.txt", data2, lastWrite, async); + } - await AddEntry(archive, "data1.txt", data1, lastWrite, async); + await DisposeZipArchive(async, archive); - e = archive.CreateEntry("empty.txt"); - e.LastWriteTime = lastWrite; + return archiveStream.ToArray(); + } - await DisposeZipArchive(async, archive); + var baseline = await WriteTestArchive(openEntryStream: false, emptyEntryAtTheEnd: false); + var test = await WriteTestArchive(openEntryStream: true, emptyEntryAtTheEnd: false); + Assert.Equal(baseline, test); - //compare - Assert.True(ArraysEqual(baseline.ToArray(), test.ToArray()), "Arrays didn't match after update"); + baseline = await WriteTestArchive(openEntryStream: false, emptyEntryAtTheEnd: true); + test = await WriteTestArchive(openEntryStream: true, emptyEntryAtTheEnd: true); + Assert.Equal(baseline, test); } [Theory] @@ -474,10 +451,10 @@ 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, ]; + yield return [49, 1, 1,]; + yield return [40, 3, 2,]; + yield return [30, 5, 3,]; + yield return [0, 8, 1,]; } [Theory] @@ -657,9 +634,9 @@ public static IEnumerable Get_Update_PerformMinimalWritesWhenEntryData public static IEnumerable Get_PerformMinimalWritesWithDataAndHeaderChanges_Data() { - yield return [ 0, 0 ]; - yield return [ 20, 40 ]; - yield return [ 30, 10 ]; + yield return [0, 0]; + yield return [20, 40]; + yield return [30, 10]; } [Theory] @@ -914,11 +891,11 @@ public async Task Update_PerformMinimalWritesWhenArchiveCommentChanged_Async() 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 ]; + yield return [-1, 40]; + yield return [-1, 49]; + yield return [-1, 0]; + yield return [42, 40]; + yield return [38, 40]; } [Theory] @@ -1109,10 +1086,10 @@ public async Task Update_PerformMinimalWritesWhenEntriesModifiedAndDeleted_Async public static IEnumerable Get_Update_PerformMinimalWritesWhenEntriesModifiedAndAdded_Data() { - yield return [ 1 ]; - yield return [ 5 ]; - yield return [ 10 ]; - yield return [ 12 ]; + yield return [1]; + yield return [5]; + yield return [10]; + yield return [12]; } [Theory]