1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 6 // Zip Spec here: http://www.pkware.com/documents/casestudies/APPNOTE.TXT 7 8 using System.Collections.Generic; 9 using System.Collections.ObjectModel; 10 using System.Diagnostics; 11 using System.Text; 12 13 namespace System.IO.Compression 14 { 15 public class ZipArchive : IDisposable 16 { 17 private Stream _archiveStream; 18 private ZipArchiveEntry _archiveStreamOwner; 19 private BinaryReader _archiveReader; 20 private ZipArchiveMode _mode; 21 private List<ZipArchiveEntry> _entries; 22 private ReadOnlyCollection<ZipArchiveEntry> _entriesCollection; 23 private Dictionary<string, ZipArchiveEntry> _entriesDictionary; 24 private bool _readEntries; 25 private bool _leaveOpen; 26 private long _centralDirectoryStart; //only valid after ReadCentralDirectory 27 private bool _isDisposed; 28 private uint _numberOfThisDisk; //only valid after ReadCentralDirectory 29 private long _expectedNumberOfEntries; 30 private Stream _backingStream; 31 private byte[] _archiveComment; 32 private Encoding _entryNameEncoding; 33 34 #if DEBUG_FORCE_ZIP64 35 public bool _forceZip64; 36 #endif 37 38 /// <summary> 39 /// Initializes a new instance of ZipArchive on the given stream for reading. 40 /// </summary> 41 /// <exception cref="ArgumentException">The stream is already closed or does not support reading.</exception> 42 /// <exception cref="ArgumentNullException">The stream is null.</exception> 43 /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip archive.</exception> 44 /// <param name="stream">The stream containing the archive to be read.</param> ZipArchive(Stream stream)45 public ZipArchive(Stream stream) : this(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null) { } 46 47 /// <summary> 48 /// Initializes a new instance of ZipArchive on the given stream in the specified mode. 49 /// </summary> 50 /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabilities of the stream.</exception> 51 /// <exception cref="ArgumentNullException">The stream is null.</exception> 52 /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception> 53 /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -or- mode is Update and an entry is missing from the archive or is corrupt and cannot be read. -or- mode is Update and an entry is too large to fit into memory.</exception> 54 /// <param name="stream">The input or output stream.</param> 55 /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support reading, Create requires the stream to support writing, and Update requires the stream to support reading, writing, and seeking.</param> ZipArchive(Stream stream, ZipArchiveMode mode)56 public ZipArchive(Stream stream, ZipArchiveMode mode) : this(stream, mode, leaveOpen: false, entryNameEncoding: null) { } 57 58 /// <summary> 59 /// Initializes a new instance of ZipArchive on the given stream in the specified mode, specifying whether to leave the stream open. 60 /// </summary> 61 /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabilities of the stream.</exception> 62 /// <exception cref="ArgumentNullException">The stream is null.</exception> 63 /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception> 64 /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -or- mode is Update and an entry is missing from the archive or is corrupt and cannot be read. -or- mode is Update and an entry is too large to fit into memory.</exception> 65 /// <param name="stream">The input or output stream.</param> 66 /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support reading, Create requires the stream to support writing, and Update requires the stream to support reading, writing, and seeking.</param> 67 /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param> ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen)68 public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen) : this(stream, mode, leaveOpen, entryNameEncoding: null) { } 69 70 /// <summary> 71 /// Initializes a new instance of ZipArchive on the given stream in the specified mode, specifying whether to leave the stream open. 72 /// </summary> 73 /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabilities of the stream.</exception> 74 /// <exception cref="ArgumentNullException">The stream is null.</exception> 75 /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception> 76 /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -or- mode is Update and an entry is missing from the archive or is corrupt and cannot be read. -or- mode is Update and an entry is too large to fit into memory.</exception> 77 /// <param name="stream">The input or output stream.</param> 78 /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support reading, Create requires the stream to support writing, and Update requires the stream to support reading, writing, and seeking.</param> 79 /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param> 80 /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names in this ZipArchive. 81 /// /// <para>NOTE: Specifying this parameter to values other than <c>null</c> is discouraged. 82 /// However, this may be necessary for interoperability with ZIP archive tools and libraries that do not correctly support 83 /// UTF-8 encoding for entry names.<br /> 84 /// This value is used as follows:</para> 85 /// <para><strong>Reading (opening) ZIP archive files:</strong></para> 86 /// <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para> 87 /// <list> 88 /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local file header is <em>not</em> set, 89 /// use the current system default code page (<c>Encoding.Default</c>) in order to decode the entry name.</item> 90 /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local file header <em>is</em> set, 91 /// use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name.</item> 92 /// </list> 93 /// <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para> 94 /// <list> 95 /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local file header is <em>not</em> set, 96 /// use the specified <c>entryNameEncoding</c> in order to decode the entry name.</item> 97 /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local file header <em>is</em> set, 98 /// use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name.</item> 99 /// </list> 100 /// <para><strong>Writing (saving) ZIP archive files:</strong></para> 101 /// <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para> 102 /// <list> 103 /// <item>For entry names that contain characters outside the ASCII range, 104 /// the language encoding flag (EFS) will be set in the general purpose bit flag of the local file header, 105 /// and UTF-8 (<c>Encoding.UTF8</c>) will be used in order to encode the entry name into bytes.</item> 106 /// <item>For entry names that do not contain characters outside the ASCII range, 107 /// the language encoding flag (EFS) will not be set in the general purpose bit flag of the local file header, 108 /// and the current system default code page (<c>Encoding.Default</c>) will be used to encode the entry names into bytes.</item> 109 /// </list> 110 /// <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para> 111 /// <list> 112 /// <item>The specified <c>entryNameEncoding</c> will always be used to encode the entry names into bytes. 113 /// The language encoding flag (EFS) in the general purpose bit flag of the local file header will be set if and only 114 /// if the specified <c>entryNameEncoding</c> is a UTF-8 encoding.</item> 115 /// </list> 116 /// <para>Note that Unicode encodings other than UTF-8 may not be currently used for the <c>entryNameEncoding</c>, 117 /// otherwise an <see cref="ArgumentException"/> is thrown.</para> 118 /// </param> 119 /// <exception cref="ArgumentException">If a Unicode encoding other than UTF-8 is specified for the <code>entryNameEncoding</code>.</exception> ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding entryNameEncoding)120 public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding entryNameEncoding) 121 { 122 if (stream == null) 123 throw new ArgumentNullException(nameof(stream)); 124 125 EntryNameEncoding = entryNameEncoding; 126 Init(stream, mode, leaveOpen); 127 } 128 129 /// <summary> 130 /// The collection of entries that are currently in the ZipArchive. This may not accurately represent the actual entries that are present in the underlying file or stream. 131 /// </summary> 132 /// <exception cref="NotSupportedException">The ZipArchive does not support reading.</exception> 133 /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> 134 /// <exception cref="InvalidDataException">The Zip archive is corrupt and the entries cannot be retrieved.</exception> 135 public ReadOnlyCollection<ZipArchiveEntry> Entries 136 { 137 get 138 { 139 if (_mode == ZipArchiveMode.Create) 140 throw new NotSupportedException(SR.EntriesInCreateMode); 141 142 ThrowIfDisposed(); 143 144 EnsureCentralDirectoryRead(); 145 return _entriesCollection; 146 } 147 } 148 149 /// <summary> 150 /// The ZipArchiveMode that the ZipArchive was initialized with. 151 /// </summary> 152 public ZipArchiveMode Mode 153 { 154 get 155 { 156 return _mode; 157 } 158 } 159 160 /// <summary> 161 /// Creates an empty entry in the Zip archive with the specified entry name. 162 /// There are no restrictions on the names of entries. 163 /// The last write time of the entry is set to the current time. 164 /// If an entry with the specified name already exists in the archive, a second entry will be created that has an identical name. 165 /// Since no <code>CompressionLevel</code> is specified, the default provided by the implementation of the underlying compression 166 /// algorithm will be used; the <code>ZipArchive</code> will not impose its own default. 167 /// (Currently, the underlying compression algorithm is provided by the <code>System.IO.Compression.DeflateStream</code> class.) 168 /// </summary> 169 /// <exception cref="ArgumentException">entryName is a zero-length string.</exception> 170 /// <exception cref="ArgumentNullException">entryName is null.</exception> 171 /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception> 172 /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> 173 /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be created.</param> 174 /// <returns>A wrapper for the newly created file entry in the archive.</returns> CreateEntry(string entryName)175 public ZipArchiveEntry CreateEntry(string entryName) 176 { 177 return DoCreateEntry(entryName, null); 178 } 179 180 /// <summary> 181 /// Creates an empty entry in the Zip archive with the specified entry name. There are no restrictions on the names of entries. The last write time of the entry is set to the current time. If an entry with the specified name already exists in the archive, a second entry will be created that has an identical name. 182 /// </summary> 183 /// <exception cref="ArgumentException">entryName is a zero-length string.</exception> 184 /// <exception cref="ArgumentNullException">entryName is null.</exception> 185 /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception> 186 /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> 187 /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be created.</param> 188 /// <param name="compressionLevel">The level of the compression (speed/memory vs. compressed size trade-off).</param> 189 /// <returns>A wrapper for the newly created file entry in the archive.</returns> CreateEntry(string entryName, CompressionLevel compressionLevel)190 public ZipArchiveEntry CreateEntry(string entryName, CompressionLevel compressionLevel) 191 { 192 return DoCreateEntry(entryName, compressionLevel); 193 } 194 195 /// <summary> 196 /// Releases the unmanaged resources used by ZipArchive and optionally finishes writing the archive and releases the managed resources. 197 /// </summary> 198 /// <param name="disposing">true to finish writing the archive and release unmanaged and managed resources, false to release only unmanaged resources.</param> Dispose(bool disposing)199 protected virtual void Dispose(bool disposing) 200 { 201 if (disposing && !_isDisposed) 202 { 203 try 204 { 205 switch (_mode) 206 { 207 case ZipArchiveMode.Read: 208 break; 209 case ZipArchiveMode.Create: 210 case ZipArchiveMode.Update: 211 default: 212 Debug.Assert(_mode == ZipArchiveMode.Update || _mode == ZipArchiveMode.Create); 213 WriteFile(); 214 break; 215 } 216 } 217 finally 218 { 219 CloseStreams(); 220 _isDisposed = true; 221 } 222 } 223 } 224 225 /// <summary> 226 /// 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. 227 /// </summary> Dispose()228 public void Dispose() 229 { 230 Dispose(true); 231 GC.SuppressFinalize(this); 232 } 233 234 /// <summary> 235 /// Retrieves a wrapper for the file entry in the archive with the specified name. Names are compared using ordinal comparison. If there are multiple entries in the archive with the specified name, the first one found will be returned. 236 /// </summary> 237 /// <exception cref="ArgumentException">entryName is a zero-length string.</exception> 238 /// <exception cref="ArgumentNullException">entryName is null.</exception> 239 /// <exception cref="NotSupportedException">The ZipArchive does not support reading.</exception> 240 /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> 241 /// <exception cref="InvalidDataException">The Zip archive is corrupt and the entries cannot be retrieved.</exception> 242 /// <param name="entryName">A path relative to the root of the archive, identifying the desired entry.</param> 243 /// <returns>A wrapper for the file entry in the archive. If no entry in the archive exists with the specified name, null will be returned.</returns> GetEntry(string entryName)244 public ZipArchiveEntry GetEntry(string entryName) 245 { 246 if (entryName == null) 247 throw new ArgumentNullException(nameof(entryName)); 248 249 if (_mode == ZipArchiveMode.Create) 250 throw new NotSupportedException(SR.EntriesInCreateMode); 251 252 EnsureCentralDirectoryRead(); 253 ZipArchiveEntry result; 254 _entriesDictionary.TryGetValue(entryName, out result); 255 return result; 256 } 257 258 internal BinaryReader ArchiveReader => _archiveReader; 259 260 internal Stream ArchiveStream => _archiveStream; 261 262 internal uint NumberOfThisDisk => _numberOfThisDisk; 263 264 internal Encoding EntryNameEncoding 265 { 266 get { return _entryNameEncoding; } 267 268 private set 269 { 270 // value == null is fine. This means the user does not want to overwrite default encoding picking logic. 271 272 // The Zip file spec [http://www.pkware.com/documents/casestudies/APPNOTE.TXT] specifies a bit in the entry header 273 // (specifically: the language encoding flag (EFS) in the general purpose bit flag of the local file header) that 274 // basically says: UTF8 (1) or CP437 (0). But in reality, tools replace CP437 with "something else that is not UTF8". 275 // For instance, the Windows Shell Zip tool takes "something else" to mean "the local system codepage". 276 // We default to the same behaviour, but we let the user explicitly specify the encoding to use for cases where they 277 // understand their use case well enough. 278 // Since the definition of acceptable encodings for the "something else" case is in reality by convention, it is not 279 // immediately clear, whether non-UTF8 Unicode encodings are acceptable. To determine that we would need to survey 280 // what is currently being done in the field, but we do not have the time for it right now. 281 // So, we artificially disallow non-UTF8 Unicode encodings for now to make sure we are not creating a compat burden 282 // for something other tools do not support. If we realise in future that "something else" should include non-UTF8 283 // Unicode encodings, we can remove this restriction. 284 285 if (value != null && 286 (value.Equals(Encoding.BigEndianUnicode) 287 || value.Equals(Encoding.Unicode) 288 #if FEATURE_UTF32 289 || value.Equals(Encoding.UTF32) 290 #endif // FEATURE_UTF32 291 #if FEATURE_UTF7 292 || value.Equals(Encoding.UTF7) 293 #endif // FEATURE_UTF7 294 )) 295 { 296 throw new ArgumentException(SR.EntryNameEncodingNotSupported, nameof(EntryNameEncoding)); 297 } 298 299 _entryNameEncoding = value; 300 } 301 } 302 DoCreateEntry(string entryName, CompressionLevel? compressionLevel)303 private ZipArchiveEntry DoCreateEntry(string entryName, CompressionLevel? compressionLevel) 304 { 305 if (entryName == null) 306 throw new ArgumentNullException(nameof(entryName)); 307 308 if (string.IsNullOrEmpty(entryName)) 309 throw new ArgumentException(SR.CannotBeEmpty, nameof(entryName)); 310 311 if (_mode == ZipArchiveMode.Read) 312 throw new NotSupportedException(SR.CreateInReadMode); 313 314 ThrowIfDisposed(); 315 316 317 ZipArchiveEntry entry = compressionLevel.HasValue ? 318 new ZipArchiveEntry(this, entryName, compressionLevel.Value) : 319 new ZipArchiveEntry(this, entryName); 320 321 AddEntry(entry); 322 323 return entry; 324 } 325 AcquireArchiveStream(ZipArchiveEntry entry)326 internal void AcquireArchiveStream(ZipArchiveEntry entry) 327 { 328 // if a previous entry had held the stream but never wrote anything, we write their local header for them 329 if (_archiveStreamOwner != null) 330 { 331 if (!_archiveStreamOwner.EverOpenedForWrite) 332 { 333 _archiveStreamOwner.WriteAndFinishLocalEntry(); 334 } 335 else 336 { 337 throw new IOException(SR.CreateModeCreateEntryWhileOpen); 338 } 339 } 340 341 _archiveStreamOwner = entry; 342 } 343 AddEntry(ZipArchiveEntry entry)344 private void AddEntry(ZipArchiveEntry entry) 345 { 346 _entries.Add(entry); 347 348 string entryName = entry.FullName; 349 if (!_entriesDictionary.ContainsKey(entryName)) 350 { 351 _entriesDictionary.Add(entryName, entry); 352 } 353 } 354 355 [Conditional("DEBUG")] 356 internal void DebugAssertIsStillArchiveStreamOwner(ZipArchiveEntry entry) => Debug.Assert(_archiveStreamOwner == entry); 357 ReleaseArchiveStream(ZipArchiveEntry entry)358 internal void ReleaseArchiveStream(ZipArchiveEntry entry) 359 { 360 Debug.Assert(_archiveStreamOwner == entry); 361 362 _archiveStreamOwner = null; 363 } 364 RemoveEntry(ZipArchiveEntry entry)365 internal void RemoveEntry(ZipArchiveEntry entry) 366 { 367 _entries.Remove(entry); 368 369 _entriesDictionary.Remove(entry.FullName); 370 } 371 ThrowIfDisposed()372 internal void ThrowIfDisposed() 373 { 374 if (_isDisposed) 375 throw new ObjectDisposedException(GetType().ToString()); 376 } 377 CloseStreams()378 private void CloseStreams() 379 { 380 if (!_leaveOpen) 381 { 382 _archiveStream.Dispose(); 383 _backingStream?.Dispose(); 384 _archiveReader?.Dispose(); 385 } 386 else 387 { 388 // if _backingStream isn't null, that means we assigned the original stream they passed 389 // us to _backingStream (which they requested we leave open), and _archiveStream was 390 // the temporary copy that we needed 391 if (_backingStream != null) 392 _archiveStream.Dispose(); 393 } 394 } 395 EnsureCentralDirectoryRead()396 private void EnsureCentralDirectoryRead() 397 { 398 if (!_readEntries) 399 { 400 ReadCentralDirectory(); 401 _readEntries = true; 402 } 403 } 404 Init(Stream stream, ZipArchiveMode mode, bool leaveOpen)405 private void Init(Stream stream, ZipArchiveMode mode, bool leaveOpen) 406 { 407 Stream extraTempStream = null; 408 409 try 410 { 411 _backingStream = null; 412 413 // check stream against mode 414 switch (mode) 415 { 416 case ZipArchiveMode.Create: 417 if (!stream.CanWrite) 418 throw new ArgumentException(SR.CreateModeCapabilities); 419 break; 420 case ZipArchiveMode.Read: 421 if (!stream.CanRead) 422 throw new ArgumentException(SR.ReadModeCapabilities); 423 if (!stream.CanSeek) 424 { 425 _backingStream = stream; 426 extraTempStream = stream = new MemoryStream(); 427 _backingStream.CopyTo(stream); 428 stream.Seek(0, SeekOrigin.Begin); 429 } 430 break; 431 case ZipArchiveMode.Update: 432 if (!stream.CanRead || !stream.CanWrite || !stream.CanSeek) 433 throw new ArgumentException(SR.UpdateModeCapabilities); 434 break; 435 default: 436 // still have to throw this, because stream constructor doesn't do mode argument checks 437 throw new ArgumentOutOfRangeException(nameof(mode)); 438 } 439 440 _mode = mode; 441 if (mode == ZipArchiveMode.Create && !stream.CanSeek) 442 _archiveStream = new PositionPreservingWriteOnlyStreamWrapper(stream); 443 else 444 _archiveStream = stream; 445 _archiveStreamOwner = null; 446 if (mode == ZipArchiveMode.Create) 447 _archiveReader = null; 448 else 449 _archiveReader = new BinaryReader(_archiveStream); 450 _entries = new List<ZipArchiveEntry>(); 451 _entriesCollection = new ReadOnlyCollection<ZipArchiveEntry>(_entries); 452 _entriesDictionary = new Dictionary<string, ZipArchiveEntry>(); 453 _readEntries = false; 454 _leaveOpen = leaveOpen; 455 _centralDirectoryStart = 0; // invalid until ReadCentralDirectory 456 _isDisposed = false; 457 _numberOfThisDisk = 0; // invalid until ReadCentralDirectory 458 _archiveComment = null; 459 460 switch (mode) 461 { 462 case ZipArchiveMode.Create: 463 _readEntries = true; 464 break; 465 case ZipArchiveMode.Read: 466 ReadEndOfCentralDirectory(); 467 break; 468 case ZipArchiveMode.Update: 469 default: 470 Debug.Assert(mode == ZipArchiveMode.Update); 471 if (_archiveStream.Length == 0) 472 { 473 _readEntries = true; 474 } 475 else 476 { 477 ReadEndOfCentralDirectory(); 478 EnsureCentralDirectoryRead(); 479 foreach (ZipArchiveEntry entry in _entries) 480 { 481 entry.ThrowIfNotOpenable(needToUncompress: false, needToLoadIntoMemory: true); 482 } 483 } 484 break; 485 } 486 } 487 catch 488 { 489 if (extraTempStream != null) 490 extraTempStream.Dispose(); 491 492 throw; 493 } 494 } 495 ReadCentralDirectory()496 private void ReadCentralDirectory() 497 { 498 try 499 { 500 // assume ReadEndOfCentralDirectory has been called and has populated _centralDirectoryStart 501 502 _archiveStream.Seek(_centralDirectoryStart, SeekOrigin.Begin); 503 504 long numberOfEntries = 0; 505 506 //read the central directory 507 ZipCentralDirectoryFileHeader currentHeader; 508 bool saveExtraFieldsAndComments = Mode == ZipArchiveMode.Update; 509 while (ZipCentralDirectoryFileHeader.TryReadBlock(_archiveReader, 510 saveExtraFieldsAndComments, out currentHeader)) 511 { 512 AddEntry(new ZipArchiveEntry(this, currentHeader)); 513 numberOfEntries++; 514 } 515 516 if (numberOfEntries != _expectedNumberOfEntries) 517 throw new InvalidDataException(SR.NumEntriesWrong); 518 } 519 catch (EndOfStreamException ex) 520 { 521 throw new InvalidDataException(SR.Format(SR.CentralDirectoryInvalid, ex)); 522 } 523 } 524 525 // This function reads all the EOCD stuff it needs to find the offset to the start of the central directory 526 // This offset gets put in _centralDirectoryStart and the number of this disk gets put in _numberOfThisDisk 527 // Also does some verification that this isn't a split/spanned archive 528 // Also checks that offset to CD isn't out of bounds ReadEndOfCentralDirectory()529 private void ReadEndOfCentralDirectory() 530 { 531 try 532 { 533 // this seeks to the start of the end of central directory record 534 _archiveStream.Seek(-ZipEndOfCentralDirectoryBlock.SizeOfBlockWithoutSignature, SeekOrigin.End); 535 if (!ZipHelper.SeekBackwardsToSignature(_archiveStream, ZipEndOfCentralDirectoryBlock.SignatureConstant)) 536 throw new InvalidDataException(SR.EOCDNotFound); 537 538 long eocdStart = _archiveStream.Position; 539 540 // read the EOCD 541 ZipEndOfCentralDirectoryBlock eocd; 542 bool eocdProper = ZipEndOfCentralDirectoryBlock.TryReadBlock(_archiveReader, out eocd); 543 Debug.Assert(eocdProper); // we just found this using the signature finder, so it should be okay 544 545 if (eocd.NumberOfThisDisk != eocd.NumberOfTheDiskWithTheStartOfTheCentralDirectory) 546 throw new InvalidDataException(SR.SplitSpanned); 547 548 _numberOfThisDisk = eocd.NumberOfThisDisk; 549 _centralDirectoryStart = eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber; 550 if (eocd.NumberOfEntriesInTheCentralDirectory != eocd.NumberOfEntriesInTheCentralDirectoryOnThisDisk) 551 throw new InvalidDataException(SR.SplitSpanned); 552 _expectedNumberOfEntries = eocd.NumberOfEntriesInTheCentralDirectory; 553 554 // only bother saving the comment if we are in update mode 555 if (_mode == ZipArchiveMode.Update) 556 _archiveComment = eocd.ArchiveComment; 557 558 // only bother looking for zip64 EOCD stuff if we suspect it is needed because some value is FFFFFFFFF 559 // because these are the only two values we need, we only worry about these 560 // if we don't find the zip64 EOCD, we just give up and try to use the original values 561 if (eocd.NumberOfThisDisk == ZipHelper.Mask16Bit || 562 eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber == ZipHelper.Mask32Bit || 563 eocd.NumberOfEntriesInTheCentralDirectory == ZipHelper.Mask16Bit) 564 { 565 // we need to look for zip 64 EOCD stuff 566 // seek to the zip 64 EOCD locator 567 _archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Begin); 568 // if we don't find it, assume it doesn't exist and use data from normal eocd 569 if (ZipHelper.SeekBackwardsToSignature(_archiveStream, Zip64EndOfCentralDirectoryLocator.SignatureConstant)) 570 { 571 // use locator to get to Zip64EOCD 572 Zip64EndOfCentralDirectoryLocator locator; 573 bool zip64eocdLocatorProper = Zip64EndOfCentralDirectoryLocator.TryReadBlock(_archiveReader, out locator); 574 Debug.Assert(zip64eocdLocatorProper); // we just found this using the signature finder, so it should be okay 575 576 if (locator.OffsetOfZip64EOCD > long.MaxValue) 577 throw new InvalidDataException(SR.FieldTooBigOffsetToZip64EOCD); 578 long zip64EOCDOffset = (long)locator.OffsetOfZip64EOCD; 579 580 _archiveStream.Seek(zip64EOCDOffset, SeekOrigin.Begin); 581 582 // read Zip64EOCD 583 Zip64EndOfCentralDirectoryRecord record; 584 if (!Zip64EndOfCentralDirectoryRecord.TryReadBlock(_archiveReader, out record)) 585 throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected); 586 587 _numberOfThisDisk = record.NumberOfThisDisk; 588 589 if (record.NumberOfEntriesTotal > long.MaxValue) 590 throw new InvalidDataException(SR.FieldTooBigNumEntries); 591 if (record.OffsetOfCentralDirectory > long.MaxValue) 592 throw new InvalidDataException(SR.FieldTooBigOffsetToCD); 593 if (record.NumberOfEntriesTotal != record.NumberOfEntriesOnThisDisk) 594 throw new InvalidDataException(SR.SplitSpanned); 595 596 _expectedNumberOfEntries = (long)record.NumberOfEntriesTotal; 597 _centralDirectoryStart = (long)record.OffsetOfCentralDirectory; 598 } 599 } 600 601 if (_centralDirectoryStart > _archiveStream.Length) 602 { 603 throw new InvalidDataException(SR.FieldTooBigOffsetToCD); 604 } 605 } 606 catch (EndOfStreamException ex) 607 { 608 throw new InvalidDataException(SR.CDCorrupt, ex); 609 } 610 catch (IOException ex) 611 { 612 throw new InvalidDataException(SR.CDCorrupt, ex); 613 } 614 } 615 WriteFile()616 private void WriteFile() 617 { 618 // if we are in create mode, we always set readEntries to true in Init 619 // if we are in update mode, we call EnsureCentralDirectoryRead, which sets readEntries to true 620 Debug.Assert(_readEntries); 621 622 if (_mode == ZipArchiveMode.Update) 623 { 624 List<ZipArchiveEntry> markedForDelete = new List<ZipArchiveEntry>(); 625 foreach (ZipArchiveEntry entry in _entries) 626 { 627 if (!entry.LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded()) 628 markedForDelete.Add(entry); 629 } 630 foreach (ZipArchiveEntry entry in markedForDelete) 631 entry.Delete(); 632 633 _archiveStream.Seek(0, SeekOrigin.Begin); 634 _archiveStream.SetLength(0); 635 } 636 637 foreach (ZipArchiveEntry entry in _entries) 638 { 639 entry.WriteAndFinishLocalEntry(); 640 } 641 642 long startOfCentralDirectory = _archiveStream.Position; 643 644 foreach (ZipArchiveEntry entry in _entries) 645 { 646 entry.WriteCentralDirectoryFileHeader(); 647 } 648 649 long sizeOfCentralDirectory = _archiveStream.Position - startOfCentralDirectory; 650 651 WriteArchiveEpilogue(startOfCentralDirectory, sizeOfCentralDirectory); 652 } 653 654 // writes eocd, and if needed, zip 64 eocd, zip64 eocd locator 655 // should only throw an exception in extremely exceptional cases because it is called from dispose WriteArchiveEpilogue(long startOfCentralDirectory, long sizeOfCentralDirectory)656 private void WriteArchiveEpilogue(long startOfCentralDirectory, long sizeOfCentralDirectory) 657 { 658 // determine if we need Zip 64 659 if (startOfCentralDirectory >= uint.MaxValue 660 || sizeOfCentralDirectory >= uint.MaxValue 661 || _entries.Count >= ZipHelper.Mask16Bit 662 #if DEBUG_FORCE_ZIP64 663 || _forceZip64 664 #endif 665 ) 666 { 667 // if we need zip 64, write zip 64 eocd and locator 668 long zip64EOCDRecordStart = _archiveStream.Position; 669 Zip64EndOfCentralDirectoryRecord.WriteBlock(_archiveStream, _entries.Count, startOfCentralDirectory, sizeOfCentralDirectory); 670 Zip64EndOfCentralDirectoryLocator.WriteBlock(_archiveStream, zip64EOCDRecordStart); 671 } 672 673 // write normal eocd 674 ZipEndOfCentralDirectoryBlock.WriteBlock(_archiveStream, _entries.Count, startOfCentralDirectory, sizeOfCentralDirectory, _archiveComment); 675 } 676 } 677 } 678