-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Fix ZipArchive Update mode rewriting unchanged archives on Dispose #123719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -297,10 +297,16 @@ 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(); | ||
| } | ||
|
Comment on lines
+305
to
+309
|
||
| break; | ||
| } | ||
| } | ||
|
|
@@ -379,6 +385,39 @@ private set | |
| // New entries in the archive won't change its state. | ||
| internal ChangeState Changed { get; private set; } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether the archive has been modified and needs to be written. | ||
| /// </summary> | ||
| 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; | ||
alinpahontu2912 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| // 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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,34 @@ 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); | ||||||||
| } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Marks this entry as modified, indicating that its data has changed and needs to be rewritten. | ||||||||
| /// </summary> | ||||||||
| /// <summary> | ||||||||
| /// Marks this entry as modified, indicating that its data has changed and needs to be rewritten. | ||||||||
| /// </summary> | ||||||||
|
Comment on lines
+908
to
+910
|
||||||||
| /// <summary> | |
| /// Marks this entry as modified, indicating that its data has changed and needs to be rewritten. | |
| /// </summary> |
alinpahontu2912 marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -824,8 +824,7 @@ public static IEnumerable<object[]> EmptyFiles() | |
| /// 2. EmptyFileCompressedWrongSize has | ||
| /// Deflate 0x08, _uncompressedSize 0, _compressedSize 4, compressed data: 0xBAAD0300 (just bad data) | ||
| /// ZipArchive is not expected to make any changes to the compression method of an archive entry unless | ||
| /// it's been changed. If it has been changed, ZipArchive is expected to change compression method to | ||
| /// Stored (0x00) and ignore "bad" compressed size | ||
| /// it's been modified. Opening an entry stream without writing does not trigger a rewrite. | ||
| /// </summary> | ||
| [Theory] | ||
| [MemberData(nameof(EmptyFiles))] | ||
|
|
@@ -852,30 +851,27 @@ public async Task ReadArchive_WithEmptyDeflatedFile(byte[] fileBytes, bool async | |
| zip = await CreateZipArchive(async, testStream, ZipArchiveMode.Update, leaveOpen: true); | ||
|
|
||
| var zipEntryStream = await OpenEntryStream(async, zip.Entries[0]); | ||
| // dispose after opening an entry will rewrite the archive | ||
| // Opening and disposing an entry stream without writing does not mark the archive as modified, | ||
| // so no rewrite will occur | ||
| await DisposeStream(async, zipEntryStream); | ||
|
|
||
| await DisposeZipArchive(async, zip); | ||
|
|
||
| fileContent = testStream.ToArray(); | ||
|
|
||
| // compression method should change to "uncompressed" (Stored = 0x0) | ||
| Assert.Equal(0, fileContent[8]); | ||
| // compression method should remain unchanged since we didn't write anything | ||
| Assert.Equal(firstEntryCompressionMethod, fileContent[8]); | ||
|
|
||
| // extract and check the file. should stay empty. | ||
| // extract and check the file | ||
| zip = await CreateZipArchive(async, testStream, ZipArchiveMode.Update); | ||
|
|
||
| ZipArchiveEntry entry = zip.GetEntry(ExpectedFileName); | ||
| // The entry retains its original properties since it was never modified | ||
| Assert.Equal(0, entry.Length); | ||
|
Comment on lines
+865
to
870
|
||
| Assert.Equal(0, entry.CompressedLength); | ||
| Stream entryStream = await OpenEntryStream(async, entry); | ||
| Assert.Equal(0, entryStream.Length); | ||
| await DisposeStream(async, entryStream); | ||
|
|
||
| await DisposeZipArchive(async, zip); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Opens an empty file that has a 64KB EOCD comment. | ||
| /// Adds two 64KB text entries. Verifies they can be read correctly. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Async Dispose has the same resource-retention concern as sync Dispose: when
IsModifiedis false,WriteFileAsync()is skipped and per-entry cached buffers (like_storedUncompressedData) may never be disposed/unloaded. Consider explicitly unloading per-entry buffers during DisposeAsync even when you don't rewrite.