diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs
index a3a6c6f6c78ce3..c75b57290412c4 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.Async.cs
@@ -147,10 +147,24 @@ protected virtual async ValueTask DisposeAsyncCore()
case ZipArchiveMode.Read:
break;
case ZipArchiveMode.Create:
+ await WriteFileAsync().ConfigureAwait(false);
+ break;
case ZipArchiveMode.Update:
default:
- Debug.Assert(_mode == ZipArchiveMode.Update || _mode == ZipArchiveMode.Create);
- await WriteFileAsync().ConfigureAwait(false);
+ Debug.Assert(_mode == ZipArchiveMode.Update);
+ // Only write if the archive has been modified
+ if (IsModified)
+ {
+ await WriteFileAsync().ConfigureAwait(false);
+ }
+ else
+ {
+ // Even if we didn't write, unload any entry buffers that may have been loaded
+ foreach (ZipArchiveEntry entry in _entries)
+ {
+ await entry.UnloadStreamsAsync().ConfigureAwait(false);
+ }
+ }
break;
}
}
diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs
index 6e1e36fd63b8eb..8d9e5fe6214fee 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs
@@ -297,10 +297,24 @@ protected virtual void Dispose(bool disposing)
case ZipArchiveMode.Read:
break;
case ZipArchiveMode.Create:
+ WriteFile();
+ break;
case ZipArchiveMode.Update:
default:
- Debug.Assert(_mode == ZipArchiveMode.Update || _mode == ZipArchiveMode.Create);
- WriteFile();
+ Debug.Assert(_mode == ZipArchiveMode.Update);
+ // Only write if the archive has been modified
+ if (IsModified)
+ {
+ WriteFile();
+ }
+ else
+ {
+ // Even if we didn't write, unload any entry buffers that may have been loaded
+ foreach (ZipArchiveEntry entry in _entries)
+ {
+ entry.UnloadStreams();
+ }
+ }
break;
}
}
@@ -311,7 +325,6 @@ protected virtual void Dispose(bool disposing)
}
}
}
-
///
/// Finishes writing the archive and releases all resources used by the ZipArchive object, unless the object was constructed with leaveOpen as true. Any streams from opened entries in the ZipArchive still open will throw exceptions on subsequent writes, as the underlying streams will have been closed.
///
@@ -379,6 +392,39 @@ private set
// New entries in the archive won't change its state.
internal ChangeState Changed { get; private set; }
+ ///
+ /// Determines whether the archive has been modified and needs to be written.
+ ///
+ private bool IsModified
+ {
+ get
+ {
+ // A new archive (created on empty stream) always needs to write the structure
+ if (_archiveStream.Length == 0)
+ {
+ return true;
+ }
+ // Archive-level changes (e.g., comment)
+ if (Changed != ChangeState.Unchanged)
+ {
+ return true;
+ }
+ // Any deleted entries
+ if (_firstDeletedEntryOffset != long.MaxValue)
+ {
+ return true;
+ }
+ // Check if any entry was modified or added
+ foreach (ZipArchiveEntry entry in _entries)
+ {
+ if (!entry.OriginallyInArchive || entry.Changes != ChangeState.Unchanged)
+ return true;
+ }
+
+ return false;
+ }
+ }
+
private ZipArchiveEntry DoCreateEntry(string entryName, CompressionLevel? compressionLevel)
{
ArgumentException.ThrowIfNullOrEmpty(entryName);
diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs
index 4a540ad7679611..52252360711f11 100644
--- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs
+++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.Async.cs
@@ -143,12 +143,6 @@ private async Task GetUncompressedDataAsync(CancellationToken canc
}
}
}
-
- // if they start modifying it and the compression method is not "store", we should make sure it will get deflated
- if (CompressionMethod != ZipCompressionMethod.Stored)
- {
- CompressionMethod = ZipCompressionMethod.Deflate;
- }
}
return _storedUncompressedData;
@@ -270,8 +264,6 @@ private async Task OpenInUpdateModeAsync(bool loadExistingContent
await ThrowIfNotOpenableAsync(needToUncompress: true, needToLoadIntoMemory: true, cancellationToken).ConfigureAwait(false);
}
- _everOpenedForWrite = true;
- Changes |= ZipArchive.ChangeState.StoredData;
_currentlyOpenForWrite = true;
if (loadExistingContent)
@@ -282,14 +274,15 @@ private async Task OpenInUpdateModeAsync(bool loadExistingContent
{
_storedUncompressedData?.Dispose();
_storedUncompressedData = new MemoryStream();
+ // Opening with loadExistingContent: false discards existing content, which is a modification
+ MarkAsModified();
}
_storedUncompressedData.Seek(0, SeekOrigin.Begin);
- return new WrappedStream(_storedUncompressedData, this, thisRef =>
- {
- thisRef!._currentlyOpenForWrite = false;
- });
+ return new WrappedStream(_storedUncompressedData, this,
+ onClosed: thisRef => thisRef!._currentlyOpenForWrite = false,
+ notifyEntryOnWrite: true);
}
private async Task<(bool, string?)> IsOpenableAsync(bool needToUncompress, bool needToLoadIntoMemory, CancellationToken cancellationToken)
@@ -351,6 +344,19 @@ private async Task WriteLocalFileHeaderAsync(bool isEmptyFile, bool forceW
private async Task WriteLocalFileHeaderAndDataIfNeededAsync(bool forceWrite, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
+
+ // Check if the entry's stored data was actually modified (StoredData flag is set).
+ // If _storedUncompressedData is loaded but StoredData is not set, it means the entry
+ // was opened for update but no writes occurred - we should use the original compressed bytes.
+ bool storedDataModified = (Changes & ZipArchive.ChangeState.StoredData) != 0;
+
+ // If _storedUncompressedData is loaded but not modified, clear it so we use _compressedBytes
+ if (_storedUncompressedData != null && !storedDataModified)
+ {
+ await _storedUncompressedData.DisposeAsync().ConfigureAwait(false);
+ _storedUncompressedData = null;
+ }
+
// _storedUncompressedData gets frozen here, and is what gets written to the file
if (_storedUncompressedData != null || _compressedBytes != null)
{
@@ -466,7 +472,7 @@ private ValueTask WriteDataDescriptorAsync(CancellationToken cancellationToken)
return _archive.ArchiveStream.WriteAsync(dataDescriptor.AsMemory(0, bytesToWrite), cancellationToken);
}
- private async Task UnloadStreamsAsync()
+ internal async Task UnloadStreamsAsync()
{
if (_storedUncompressedData != null)
{
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 0e077b9dc25708..fb9a34cc23ffac 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
@@ -508,16 +508,15 @@ private MemoryStream GetUncompressedData()
}
}
- // if they start modifying it and the compression method is not "store", we should make sure it will get deflated
- if (CompressionMethod != ZipCompressionMethod.Stored)
- {
- CompressionMethod = ZipCompressionMethod.Deflate;
- }
+ // NOTE: CompressionMethod normalization is deferred to MarkAsModified() to avoid
+ // corrupting entries that are opened in Update mode but not actually written to.
+ // If we normalized here and the entry wasn't modified, we'd write a header with
+ // CompressionMethod=Deflate but the original _compressedBytes would still be in
+ // their original format (e.g., Deflate64), producing an invalid entry.
}
return _storedUncompressedData;
}
-
// does almost everything you need to do to forget about this entry
// writes the local header/data, gets rid of all the data,
// closes all of the streams except for the very outermost one that
@@ -882,8 +881,6 @@ private WrappedStream OpenInUpdateMode(bool loadExistingContent = true)
ThrowIfNotOpenable(needToUncompress: true, needToLoadIntoMemory: true);
}
- _everOpenedForWrite = true;
- Changes |= ZipArchive.ChangeState.StoredData;
_currentlyOpenForWrite = true;
if (loadExistingContent)
@@ -894,14 +891,31 @@ private WrappedStream OpenInUpdateMode(bool loadExistingContent = true)
{
_storedUncompressedData?.Dispose();
_storedUncompressedData = new MemoryStream();
+ // Opening with loadExistingContent: false discards existing content, which is a modification
+ MarkAsModified();
}
_storedUncompressedData.Seek(0, SeekOrigin.Begin);
- return new WrappedStream(_storedUncompressedData, this, thisRef =>
+ return new WrappedStream(_storedUncompressedData, this,
+ onClosed: thisRef => thisRef!._currentlyOpenForWrite = false,
+ notifyEntryOnWrite: true);
+ }
+
+ ///
+ /// Marks this entry as modified, indicating that its data has changed and needs to be rewritten.
+ ///
+ internal void MarkAsModified()
+ {
+ _everOpenedForWrite = true;
+ Changes |= ZipArchive.ChangeState.StoredData;
+
+ // Normalize compression method when actually modifying - Deflate64 data will be
+ // re-compressed as Deflate since we don't support writing Deflate64.
+ if (CompressionMethod != ZipCompressionMethod.Stored)
{
- thisRef!._currentlyOpenForWrite = false;
- });
+ CompressionMethod = ZipCompressionMethod.Deflate;
+ }
}
private bool IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out string? message)
@@ -1171,6 +1185,18 @@ private bool WriteLocalFileHeader(bool isEmptyFile, bool forceWrite)
private void WriteLocalFileHeaderAndDataIfNeeded(bool forceWrite)
{
+ // Check if the entry's stored data was actually modified (StoredData flag is set).
+ // If _storedUncompressedData is loaded but StoredData is not set, it means the entry
+ // was opened for update but no writes occurred - we should use the original compressed bytes.
+ bool storedDataModified = (Changes & ZipArchive.ChangeState.StoredData) != 0;
+
+ // If _storedUncompressedData is loaded but not modified, clear it so we use _compressedBytes
+ if (_storedUncompressedData != null && !storedDataModified)
+ {
+ _storedUncompressedData.Dispose();
+ _storedUncompressedData = null;
+ }
+
// _storedUncompressedData gets frozen here, and is what gets written to the file
if (_storedUncompressedData != null || _compressedBytes != null)
{
@@ -1396,7 +1422,7 @@ private int PrepareToWriteDataDescriptor(Span dataDescriptor)
return bytesToWrite;
}
- private void UnloadStreams()
+ internal void UnloadStreams()
{
_storedUncompressedData?.Dispose();
_compressedBytes = null;
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 0514bea7dac946..12f41c5740b8cb 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
@@ -16,24 +16,28 @@ internal sealed class WrappedStream : Stream
// Delegate that will be invoked on stream disposing
private readonly Action? _onClosed;
+ // When true, notifies the entry on first write operation
+ private bool _notifyEntryOnWrite;
+
// Instance that will be passed to _onClose delegate
private readonly ZipArchiveEntry? _zipArchiveEntry;
private bool _isDisposed;
internal WrappedStream(Stream baseStream, bool closeBaseStream)
- : this(baseStream, closeBaseStream, null, null) { }
+ : this(baseStream, closeBaseStream, entry: null, onClosed: null, notifyEntryOnWrite: false) { }
- private WrappedStream(Stream baseStream, bool closeBaseStream, ZipArchiveEntry? entry, Action? onClosed)
+ private WrappedStream(Stream baseStream, bool closeBaseStream, ZipArchiveEntry? entry, Action? onClosed, bool notifyEntryOnWrite)
{
_baseStream = baseStream;
_closeBaseStream = closeBaseStream;
_onClosed = onClosed;
+ _notifyEntryOnWrite = notifyEntryOnWrite;
_zipArchiveEntry = entry;
_isDisposed = false;
}
- internal WrappedStream(Stream baseStream, ZipArchiveEntry entry, Action? onClosed)
- : this(baseStream, false, entry, onClosed) { }
+ internal WrappedStream(Stream baseStream, ZipArchiveEntry entry, Action? onClosed, bool notifyEntryOnWrite = false)
+ : this(baseStream, false, entry, onClosed, notifyEntryOnWrite) { }
public override long Length
{
@@ -144,6 +148,7 @@ public override void SetLength(long value)
ThrowIfCantSeek();
ThrowIfCantWrite();
+ NotifyWrite();
_baseStream.SetLength(value);
}
@@ -152,6 +157,7 @@ public override void Write(byte[] buffer, int offset, int count)
ThrowIfDisposed();
ThrowIfCantWrite();
+ NotifyWrite();
_baseStream.Write(buffer, offset, count);
}
@@ -160,6 +166,7 @@ public override void Write(ReadOnlySpan source)
ThrowIfDisposed();
ThrowIfCantWrite();
+ NotifyWrite();
_baseStream.Write(source);
}
@@ -168,6 +175,7 @@ public override void WriteByte(byte value)
ThrowIfDisposed();
ThrowIfCantWrite();
+ NotifyWrite();
_baseStream.WriteByte(value);
}
@@ -176,6 +184,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati
ThrowIfDisposed();
ThrowIfCantWrite();
+ NotifyWrite();
return _baseStream.WriteAsync(buffer, offset, count, cancellationToken);
}
@@ -184,9 +193,19 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo
ThrowIfDisposed();
ThrowIfCantWrite();
+ NotifyWrite();
return _baseStream.WriteAsync(buffer, cancellationToken);
}
+ private void NotifyWrite()
+ {
+ if (_notifyEntryOnWrite)
+ {
+ _zipArchiveEntry?.MarkAsModified();
+ _notifyEntryOnWrite = false; // Only notify once
+ }
+ }
+
public override void Flush()
{
ThrowIfDisposed();
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 254d7d07b0bc84..e80fc63790f938 100644
--- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs
+++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs
@@ -824,8 +824,7 @@ public static IEnumerable