@@ -30,6 +30,7 @@ public class ZipArchive : IDisposable
3030        private  readonly  Stream ?  _backingStream ; 
3131        private  byte [ ]  _archiveComment ; 
3232        private  Encoding ?  _entryNameAndCommentEncoding ; 
33+         private  long  _firstDeletedEntryOffset ; 
3334
3435#if DEBUG_FORCE_ZIP64 
3536        public  bool  _forceZip64 ; 
@@ -164,12 +165,14 @@ public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding?
164165                _entries  =  new  List < ZipArchiveEntry > ( ) ; 
165166                _entriesCollection  =  new  ReadOnlyCollection < ZipArchiveEntry > ( _entries ) ; 
166167                _entriesDictionary  =  new  Dictionary < string ,  ZipArchiveEntry > ( ) ; 
168+                 Changed  =  ChangeState . Unchanged ; 
167169                _readEntries  =  false ; 
168170                _leaveOpen  =  leaveOpen ; 
169171                _centralDirectoryStart  =  0 ;  // invalid until ReadCentralDirectory 
170172                _isDisposed  =  false ; 
171173                _numberOfThisDisk  =  0 ;  // invalid until ReadCentralDirectory 
172174                _archiveComment  =  Array . Empty < byte > ( ) ; 
175+                 _firstDeletedEntryOffset  =  long . MaxValue ; 
173176
174177                switch  ( mode ) 
175178                { 
@@ -217,7 +220,11 @@ public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding?
217220        public  string  Comment 
218221        { 
219222            get  =>  ( EntryNameAndCommentEncoding  ??  Encoding . UTF8 ) . GetString ( _archiveComment ) ; 
220-             set  =>  _archiveComment  =  ZipHelper . GetEncodedTruncatedBytesFromString ( value ,  EntryNameAndCommentEncoding ,  ZipEndOfCentralDirectoryBlock . ZipFileCommentMaxLength ,  out  _ ) ; 
223+             set 
224+             { 
225+                 _archiveComment  =  ZipHelper . GetEncodedTruncatedBytesFromString ( value ,  EntryNameAndCommentEncoding ,  ZipEndOfCentralDirectoryBlock . ZipFileCommentMaxLength ,  out  _ ) ; 
226+                 Changed  |=  ChangeState . DynamicLengthMetadata ; 
227+             } 
221228        } 
222229
223230        /// <summary> 
@@ -383,6 +390,10 @@ private set
383390            } 
384391        } 
385392
393+         // This property's value only relates to the top-level fields of the archive (such as the archive comment.) 
394+         // New entries in the archive won't change its state. 
395+         internal  ChangeState  Changed  {  get ;  private  set ;  } 
396+ 
386397        private  ZipArchiveEntry  DoCreateEntry ( string  entryName ,  CompressionLevel ?  compressionLevel ) 
387398        { 
388399            ArgumentException . ThrowIfNullOrEmpty ( entryName ) ; 
@@ -409,7 +420,7 @@ internal void AcquireArchiveStream(ZipArchiveEntry entry)
409420            { 
410421                if  ( ! _archiveStreamOwner . EverOpenedForWrite ) 
411422                { 
412-                     _archiveStreamOwner . WriteAndFinishLocalEntry ( ) ; 
423+                     _archiveStreamOwner . WriteAndFinishLocalEntry ( forceWrite :   true ) ; 
413424                } 
414425                else 
415426                { 
@@ -441,6 +452,11 @@ internal void RemoveEntry(ZipArchiveEntry entry)
441452            _entries . Remove ( entry ) ; 
442453
443454            _entriesDictionary . Remove ( entry . FullName ) ; 
455+             // Keep track of the offset of the earliest deleted entry in the archive 
456+             if  ( entry . OriginallyInArchive  &&  entry . OffsetOfLocalHeader  <  _firstDeletedEntryOffset ) 
457+             { 
458+                 _firstDeletedEntryOffset  =  entry . OffsetOfLocalHeader ; 
459+             } 
444460        } 
445461
446462        internal  void  ThrowIfDisposed ( ) 
@@ -550,7 +566,12 @@ private void ReadCentralDirectory()
550566                    throw  new  InvalidDataException ( SR . NumEntriesWrong ) ; 
551567                } 
552568
553-                 _archiveStream . Seek ( _centralDirectoryStart  +  bytesRead ,  SeekOrigin . Begin ) ; 
569+                 // Sort _entries by each archive entry's position. This supports the algorithm in WriteFile, so is only 
570+                 // necessary when the ZipArchive has been opened in Update mode. 
571+                 if  ( Mode  ==  ZipArchiveMode . Update ) 
572+                 { 
573+                     _entries . Sort ( ZipArchiveEntry . LocalHeaderOffsetComparer . Instance ) ; 
574+                 } 
554575            } 
555576            catch  ( EndOfStreamException  ex ) 
556577            { 
@@ -681,41 +702,107 @@ private void WriteFile()
681702            // if we are in update mode, we call EnsureCentralDirectoryRead, which sets readEntries to true 
682703            Debug . Assert ( _readEntries ) ; 
683704
705+             // Entries starting after this offset have had a dynamically-sized change. Everything on or after this point must be rewritten. 
706+             long  completeRewriteStartingOffset  =  0 ; 
707+             List < ZipArchiveEntry >  entriesToWrite  =  _entries ; 
708+ 
684709            if  ( _mode  ==  ZipArchiveMode . Update ) 
685710            { 
686-                 List < ZipArchiveEntry >  markedForDelete  =  new  List < ZipArchiveEntry > ( ) ; 
711+                 // Entries starting after this offset have some kind of change made to them. It might just be a fixed-length field though, in which case 
712+                 // that single entry's metadata can be rewritten without impacting anything else. 
713+                 long  startingOffset  =  _firstDeletedEntryOffset ; 
714+                 long  nextFileOffset  =  0 ; 
715+                 completeRewriteStartingOffset  =  startingOffset ; 
716+ 
717+                 entriesToWrite  =  new ( _entries . Count ) ; 
687718                foreach  ( ZipArchiveEntry  entry  in  _entries ) 
688719                { 
689-                     if  ( ! entry . LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded ( ) ) 
690-                         markedForDelete . Add ( entry ) ; 
720+                     if  ( ! entry . OriginallyInArchive ) 
721+                     { 
722+                         entriesToWrite . Add ( entry ) ; 
723+                     } 
724+                     else 
725+                     { 
726+                         if  ( entry . Changes  ==  ChangeState . Unchanged ) 
727+                         { 
728+                             // Keep track of the expected position of the file entry after the final untouched file entry so that when the loop completes, 
729+                             // we'll know which position to start writing new entries from. 
730+                             nextFileOffset  =  Math . Max ( nextFileOffset ,  entry . OffsetOfCompressedData  +  entry . CompressedLength ) ; 
731+                         } 
732+                         // When calculating the starting offset to load the files from, only look at changed entries which are already in the archive. 
733+                         else 
734+                         { 
735+                             startingOffset  =  Math . Min ( startingOffset ,  entry . OffsetOfLocalHeader ) ; 
736+                         } 
737+ 
738+                         // We want to re-write entries which are after the starting offset of the first entry which has pending data to write. 
739+                         // NB: the existing ZipArchiveEntries are sorted in _entries by their position ascending. 
740+                         if  ( entry . OffsetOfLocalHeader  >=  startingOffset ) 
741+                         { 
742+                             // If the pending data to write is fixed-length metadata in the header, there's no need to load the compressed file bits. 
743+                             if  ( ( entry . Changes  &  ( ChangeState . DynamicLengthMetadata  |  ChangeState . StoredData ) )  !=  0 ) 
744+                             { 
745+                                 completeRewriteStartingOffset  =  Math . Min ( completeRewriteStartingOffset ,  entry . OffsetOfLocalHeader ) ; 
746+                             } 
747+                             if  ( entry . OffsetOfLocalHeader  >=  completeRewriteStartingOffset ) 
748+                             { 
749+                                 entry . LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded ( ) ; 
750+                             } 
751+ 
752+                             entriesToWrite . Add ( entry ) ; 
753+                         } 
754+                     } 
755+                 } 
756+ 
757+                 // If the offset of entries to write from is still at long.MaxValue, then we know that nothing has been deleted, 
758+                 // nothing has been modified - so we just want to move to the end of all remaining files in the archive. 
759+                 if  ( startingOffset  ==  long . MaxValue ) 
760+                 { 
761+                     startingOffset  =  nextFileOffset ; 
691762                } 
692-                 foreach  ( ZipArchiveEntry  entry  in  markedForDelete ) 
693-                     entry . Delete ( ) ; 
694763
695-                 _archiveStream . Seek ( 0 ,  SeekOrigin . Begin ) ; 
696-                 _archiveStream . SetLength ( 0 ) ; 
764+                 _archiveStream . Seek ( startingOffset ,  SeekOrigin . Begin ) ; 
697765            } 
698766
699-             foreach  ( ZipArchiveEntry  entry  in  _entries ) 
767+             foreach  ( ZipArchiveEntry  entry  in  entriesToWrite ) 
700768            { 
701-                 entry . WriteAndFinishLocalEntry ( ) ; 
769+                 // We don't always need to write the local header entry, ZipArchiveEntry is usually able to work out when it doesn't need to. 
770+                 // We want to force this header entry to be written (even for completely untouched entries) if the entry comes after one 
771+                 // which had a pending dynamically-sized write. 
772+                 bool  forceWriteLocalEntry  =  ! entry . OriginallyInArchive  ||  ( entry . OriginallyInArchive  &&  entry . OffsetOfLocalHeader  >=  completeRewriteStartingOffset ) ; 
773+ 
774+                 entry . WriteAndFinishLocalEntry ( forceWriteLocalEntry ) ; 
702775            } 
703776
704-             long  startOfCentralDirectory  =  _archiveStream . Position ; 
777+             long  plannedCentralDirectoryPosition  =  _archiveStream . Position ; 
778+             // If there are no entries in the archive, we still want to create the archive epilogue. 
779+             bool  archiveEpilogueRequiresUpdate  =  _entries . Count  ==  0 ; 
705780
706781            foreach  ( ZipArchiveEntry  entry  in  _entries ) 
707782            { 
708-                 entry . WriteCentralDirectoryFileHeader ( ) ; 
783+                 // The central directory needs to be rewritten if its position has moved, if there's a new entry in the archive, or if the entry might be different. 
784+                 bool  centralDirectoryEntryRequiresUpdate  =  plannedCentralDirectoryPosition  !=  _centralDirectoryStart 
785+                     ||  ! entry . OriginallyInArchive  ||  entry . OffsetOfLocalHeader  >=  completeRewriteStartingOffset ; 
786+ 
787+                 entry . WriteCentralDirectoryFileHeader ( centralDirectoryEntryRequiresUpdate ) ; 
788+                 archiveEpilogueRequiresUpdate  |=  centralDirectoryEntryRequiresUpdate ; 
709789            } 
710790
711-             long  sizeOfCentralDirectory  =  _archiveStream . Position  -  startOfCentralDirectory ; 
791+             long  sizeOfCentralDirectory  =  _archiveStream . Position  -  plannedCentralDirectoryPosition ; 
792+ 
793+             WriteArchiveEpilogue ( plannedCentralDirectoryPosition ,  sizeOfCentralDirectory ,  archiveEpilogueRequiresUpdate ) ; 
712794
713-             WriteArchiveEpilogue ( startOfCentralDirectory ,  sizeOfCentralDirectory ) ; 
795+             // If entries have been removed and new (smaller) ones added, there could be empty space at the end of the file. 
796+             // Shrink the file to reclaim this space. 
797+             if  ( _mode  ==  ZipArchiveMode . Update  &&  _archiveStream . Position  !=  _archiveStream . Length ) 
798+             { 
799+                 _archiveStream . SetLength ( _archiveStream . Position ) ; 
800+             } 
714801        } 
715802
716803        // writes eocd, and if needed, zip 64 eocd, zip64 eocd locator 
717804        // should only throw an exception in extremely exceptional cases because it is called from dispose 
718-         private  void  WriteArchiveEpilogue ( long  startOfCentralDirectory ,  long  sizeOfCentralDirectory ) 
805+         private  void  WriteArchiveEpilogue ( long  startOfCentralDirectory ,  long  sizeOfCentralDirectory ,   bool   centralDirectoryChanged ) 
719806        { 
720807            // determine if we need Zip 64 
721808            if  ( startOfCentralDirectory  >=  uint . MaxValue 
@@ -728,12 +815,37 @@ private void WriteArchiveEpilogue(long startOfCentralDirectory, long sizeOfCentr
728815            { 
729816                // if we need zip 64, write zip 64 eocd and locator 
730817                long  zip64EOCDRecordStart  =  _archiveStream . Position ; 
731-                 Zip64EndOfCentralDirectoryRecord . WriteBlock ( _archiveStream ,  _entries . Count ,  startOfCentralDirectory ,  sizeOfCentralDirectory ) ; 
732-                 Zip64EndOfCentralDirectoryLocator . WriteBlock ( _archiveStream ,  zip64EOCDRecordStart ) ; 
818+ 
819+                 if  ( centralDirectoryChanged ) 
820+                 { 
821+                     Zip64EndOfCentralDirectoryRecord . WriteBlock ( _archiveStream ,  _entries . Count ,  startOfCentralDirectory ,  sizeOfCentralDirectory ) ; 
822+                     Zip64EndOfCentralDirectoryLocator . WriteBlock ( _archiveStream ,  zip64EOCDRecordStart ) ; 
823+                 } 
824+                 else 
825+                 { 
826+                     _archiveStream . Seek ( Zip64EndOfCentralDirectoryRecord . TotalSize ,  SeekOrigin . Current ) ; 
827+                     _archiveStream . Seek ( Zip64EndOfCentralDirectoryLocator . TotalSize ,  SeekOrigin . Current ) ; 
828+                 } 
733829            } 
734830
735831            // write normal eocd 
736-             ZipEndOfCentralDirectoryBlock . WriteBlock ( _archiveStream ,  _entries . Count ,  startOfCentralDirectory ,  sizeOfCentralDirectory ,  _archiveComment ) ; 
832+             if  ( centralDirectoryChanged  ||  ( Changed  !=  ChangeState . Unchanged ) ) 
833+             { 
834+                 ZipEndOfCentralDirectoryBlock . WriteBlock ( _archiveStream ,  _entries . Count ,  startOfCentralDirectory ,  sizeOfCentralDirectory ,  _archiveComment ) ; 
835+             } 
836+             else 
837+             { 
838+                 _archiveStream . Seek ( ZipEndOfCentralDirectoryBlock . TotalSize  +  _archiveComment . Length ,  SeekOrigin . Current ) ; 
839+             } 
840+         } 
841+ 
842+         [ Flags ] 
843+         internal  enum  ChangeState 
844+         { 
845+             Unchanged  =  0x0 , 
846+             FixedLengthMetadata  =  0x1 , 
847+             DynamicLengthMetadata  =  0x2 , 
848+             StoredData  =  0x4 
737849        } 
738850    } 
739851} 
0 commit comments