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