| | | 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 | | |
| | | 4 | | |
| | | 5 | | // Zip Spec here: http://www.pkware.com/documents/casestudies/APPNOTE.TXT |
| | | 6 | | |
| | | 7 | | using System.Buffers; |
| | | 8 | | using System.Collections.Generic; |
| | | 9 | | using System.Collections.ObjectModel; |
| | | 10 | | using System.ComponentModel; |
| | | 11 | | using System.Diagnostics; |
| | | 12 | | using System.Diagnostics.CodeAnalysis; |
| | | 13 | | using System.Text; |
| | | 14 | | |
| | | 15 | | namespace System.IO.Compression |
| | | 16 | | { |
| | | 17 | | public partial class ZipArchive : IDisposable, IAsyncDisposable |
| | | 18 | | { |
| | | 19 | | private const int ReadCentralDirectoryReadBufferSize = 4096; |
| | | 20 | | |
| | | 21 | | private readonly Stream _archiveStream; |
| | | 22 | | private ZipArchiveEntry? _archiveStreamOwner; |
| | | 23 | | private readonly ZipArchiveMode _mode; |
| | | 24 | | private readonly List<ZipArchiveEntry> _entries; |
| | | 25 | | private readonly ReadOnlyCollection<ZipArchiveEntry> _entriesCollection; |
| | | 26 | | private readonly Dictionary<string, ZipArchiveEntry> _entriesDictionary; |
| | | 27 | | private bool _readEntries; |
| | | 28 | | private readonly bool _leaveOpen; |
| | | 29 | | private long _centralDirectoryStart; // only valid after ReadCentralDirectory |
| | | 30 | | private bool _isDisposed; |
| | | 31 | | private uint _numberOfThisDisk; // only valid after ReadCentralDirectory |
| | | 32 | | private long _expectedNumberOfEntries; |
| | | 33 | | private readonly Stream? _backingStream; |
| | | 34 | | private byte[] _archiveComment; |
| | | 35 | | private Encoding? _entryNameAndCommentEncoding; |
| | | 36 | | private long _firstDeletedEntryOffset; |
| | | 37 | | |
| | | 38 | | #if DEBUG_FORCE_ZIP64 |
| | | 39 | | public bool _forceZip64; |
| | | 40 | | #endif |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Initializes a new instance of ZipArchive on the given stream for reading. |
| | | 44 | | /// </summary> |
| | | 45 | | /// <exception cref="ArgumentException">The stream is already closed or does not support reading.</exception> |
| | | 46 | | /// <exception cref="ArgumentNullException">The stream is null.</exception> |
| | | 47 | | /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip archive. |
| | | 48 | | /// <param name="stream">The stream containing the archive to be read.</param> |
| | 0 | 49 | | public ZipArchive(Stream stream) : this(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null) |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// Initializes a new instance of ZipArchive on the given stream in the specified mode. |
| | | 53 | | /// </summary> |
| | | 54 | | /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabil |
| | | 55 | | /// <exception cref="ArgumentNullException">The stream is null.</exception> |
| | | 56 | | /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception> |
| | | 57 | | /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -o |
| | | 58 | | /// <param name="stream">The input or output stream.</param> |
| | | 59 | | /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support readi |
| | 0 | 60 | | public ZipArchive(Stream stream, ZipArchiveMode mode) : this(stream, mode, leaveOpen: false, entryNameEncoding: |
| | | 61 | | |
| | | 62 | | /// <summary> |
| | | 63 | | /// Initializes a new instance of ZipArchive on the given stream in the specified mode, specifying whether to le |
| | | 64 | | /// </summary> |
| | | 65 | | /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabil |
| | | 66 | | /// <exception cref="ArgumentNullException">The stream is null.</exception> |
| | | 67 | | /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception> |
| | | 68 | | /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -o |
| | | 69 | | /// <param name="stream">The input or output stream.</param> |
| | | 70 | | /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support readi |
| | | 71 | | /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param |
| | 0 | 72 | | public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen) : this(stream, mode, leaveOpen, entryNameE |
| | | 73 | | |
| | | 74 | | /// <summary> |
| | | 75 | | /// Initializes a new instance of ZipArchive on the given stream in the specified mode, specifying whether to le |
| | | 76 | | /// </summary> |
| | | 77 | | /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabil |
| | | 78 | | /// <exception cref="ArgumentNullException">The stream is null.</exception> |
| | | 79 | | /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception> |
| | | 80 | | /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -o |
| | | 81 | | /// <param name="stream">The input or output stream.</param> |
| | | 82 | | /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support readi |
| | | 83 | | /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param |
| | | 84 | | /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names and comments in this |
| | | 85 | | /// /// <para>NOTE: Specifying this parameter to values other than <c>null</c> is discouraged. |
| | | 86 | | /// However, this may be necessary for interoperability with ZIP archive tools and libraries that do not |
| | | 87 | | /// UTF-8 encoding for entry names.<br /> |
| | | 88 | | /// This value is used as follows:</para> |
| | | 89 | | /// <para><strong>Reading (opening) ZIP archive files:</strong></para> |
| | | 90 | | /// <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para> |
| | | 91 | | /// <list> |
| | | 92 | | /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca |
| | | 93 | | /// use the current system default code page (<c>Encoding.Default</c>) in order to decode the entry name |
| | | 94 | | /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca |
| | | 95 | | /// use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name and comment.</item> |
| | | 96 | | /// </list> |
| | | 97 | | /// <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para> |
| | | 98 | | /// <list> |
| | | 99 | | /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca |
| | | 100 | | /// use the specified <c>entryNameEncoding</c> in order to decode the entry name and comment.</item> |
| | | 101 | | /// <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca |
| | | 102 | | /// use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name and comment.</item> |
| | | 103 | | /// </list> |
| | | 104 | | /// <para><strong>Writing (saving) ZIP archive files:</strong></para> |
| | | 105 | | /// <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para> |
| | | 106 | | /// <list> |
| | | 107 | | /// <item>For entry names and comments that contain characters outside the ASCII range, |
| | | 108 | | /// the language encoding flag (EFS) will be set in the general purpose bit flag of the local file heade |
| | | 109 | | /// and UTF-8 (<c>Encoding.UTF8</c>) will be used in order to encode the entry name and comment into byt |
| | | 110 | | /// <item>For entry names and comments that do not contain characters outside the ASCII range, |
| | | 111 | | /// the language encoding flag (EFS) will not be set in the general purpose bit flag of the local file h |
| | | 112 | | /// and the current system default code page (<c>Encoding.Default</c>) will be used to encode the entry |
| | | 113 | | /// </list> |
| | | 114 | | /// <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para> |
| | | 115 | | /// <list> |
| | | 116 | | /// <item>The specified <c>entryNameEncoding</c> will always be used to encode the entry names and comme |
| | | 117 | | /// The language encoding flag (EFS) in the general purpose bit flag of the local file header will be se |
| | | 118 | | /// if the specified <c>entryNameEncoding</c> is a UTF-8 encoding.</item> |
| | | 119 | | /// </list> |
| | | 120 | | /// <para>Note that Unicode encodings other than UTF-8 may not be currently used for the <c>entryNameEncodin |
| | | 121 | | /// otherwise an <see cref="ArgumentException"/> is thrown.</para> |
| | | 122 | | /// </param> |
| | | 123 | | /// <exception cref="ArgumentException">If a Unicode encoding other than UTF-8 is specified for the <code>entryN |
| | | 124 | | public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding) |
| | 1738 | 125 | | : this(mode, leaveOpen, entryNameEncoding, backingStream: null, archiveStream: DecideArchiveStream(mode, str |
| | 1738 | 126 | | { |
| | 1738 | 127 | | ArgumentNullException.ThrowIfNull(stream); |
| | | 128 | | |
| | 1738 | 129 | | Stream? extraTempStream = null; |
| | | 130 | | |
| | | 131 | | try |
| | 1738 | 132 | | { |
| | 1738 | 133 | | _backingStream = null; |
| | | 134 | | |
| | 1738 | 135 | | if (ValidateMode(mode, stream)) |
| | 0 | 136 | | { |
| | 0 | 137 | | _backingStream = stream; |
| | 0 | 138 | | extraTempStream = stream = new MemoryStream(); |
| | 0 | 139 | | _backingStream.CopyTo(stream); |
| | 0 | 140 | | stream.Seek(0, SeekOrigin.Begin); |
| | 0 | 141 | | } |
| | | 142 | | |
| | 1738 | 143 | | _archiveStream = DecideArchiveStream(mode, stream); |
| | | 144 | | |
| | 1738 | 145 | | switch (mode) |
| | | 146 | | { |
| | | 147 | | case ZipArchiveMode.Create: |
| | 0 | 148 | | _readEntries = true; |
| | 0 | 149 | | break; |
| | | 150 | | case ZipArchiveMode.Read: |
| | 1738 | 151 | | ReadEndOfCentralDirectory(); |
| | 1605 | 152 | | break; |
| | | 153 | | case ZipArchiveMode.Update: |
| | | 154 | | default: |
| | 0 | 155 | | Debug.Assert(mode == ZipArchiveMode.Update); |
| | 0 | 156 | | if (_archiveStream.Length == 0) |
| | 0 | 157 | | { |
| | 0 | 158 | | _readEntries = true; |
| | 0 | 159 | | } |
| | | 160 | | else |
| | 0 | 161 | | { |
| | 0 | 162 | | ReadEndOfCentralDirectory(); |
| | 0 | 163 | | EnsureCentralDirectoryRead(); |
| | | 164 | | |
| | 0 | 165 | | foreach (ZipArchiveEntry entry in _entries) |
| | 0 | 166 | | { |
| | 0 | 167 | | entry.ThrowIfNotOpenable(needToUncompress: false, needToLoadIntoMemory: true); |
| | 0 | 168 | | } |
| | 0 | 169 | | } |
| | 0 | 170 | | break; |
| | | 171 | | } |
| | 1605 | 172 | | } |
| | 133 | 173 | | catch (Exception) |
| | 133 | 174 | | { |
| | 133 | 175 | | extraTempStream?.Dispose(); |
| | | 176 | | |
| | 133 | 177 | | throw; |
| | | 178 | | } |
| | 1605 | 179 | | } |
| | | 180 | | |
| | | 181 | | /// Helper constructor that initializes some of the essential ZipArchive |
| | | 182 | | /// information that other constructors initialize the same way. |
| | | 183 | | /// Validations, checks and entry collection need to be done outside this constructor. |
| | 3476 | 184 | | private ZipArchive(ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding, Stream? backingStream, Stre |
| | 3476 | 185 | | { |
| | 3476 | 186 | | _backingStream = backingStream; |
| | 3476 | 187 | | _archiveStream = archiveStream; |
| | 3476 | 188 | | _mode = mode; |
| | 3476 | 189 | | EntryNameAndCommentEncoding = entryNameEncoding; |
| | 3476 | 190 | | _archiveStreamOwner = null; |
| | 3476 | 191 | | _entries = new List<ZipArchiveEntry>(); |
| | 3476 | 192 | | _entriesCollection = new ReadOnlyCollection<ZipArchiveEntry>(_entries); |
| | 3476 | 193 | | _entriesDictionary = new Dictionary<string, ZipArchiveEntry>(); |
| | 3476 | 194 | | Changed = ChangeState.Unchanged; |
| | 3476 | 195 | | _readEntries = false; |
| | 3476 | 196 | | _leaveOpen = leaveOpen; |
| | 3476 | 197 | | _centralDirectoryStart = 0; // invalid until ReadCentralDirectory |
| | 3476 | 198 | | _isDisposed = false; |
| | 3476 | 199 | | _numberOfThisDisk = 0; // invalid until ReadCentralDirectory |
| | 3476 | 200 | | _archiveComment = Array.Empty<byte>(); |
| | 3476 | 201 | | _firstDeletedEntryOffset = long.MaxValue; |
| | 3476 | 202 | | } |
| | | 203 | | |
| | | 204 | | /// <summary> |
| | | 205 | | /// Gets or sets the optional archive comment. |
| | | 206 | | /// </summary> |
| | | 207 | | /// <remarks> |
| | | 208 | | /// The comment encoding is determined by the <c>entryNameEncoding</c> parameter of the <see cref="ZipArchive(St |
| | | 209 | | /// If the comment byte length is larger than <see cref="ushort.MaxValue"/>, it will be truncated when disposing |
| | | 210 | | /// </remarks> |
| | | 211 | | [AllowNull] |
| | | 212 | | public string Comment |
| | | 213 | | { |
| | 0 | 214 | | get => (EntryNameAndCommentEncoding ?? Encoding.UTF8).GetString(_archiveComment); |
| | | 215 | | set |
| | 0 | 216 | | { |
| | 0 | 217 | | _archiveComment = ZipHelper.GetEncodedTruncatedBytesFromString(value, EntryNameAndCommentEncoding, ZipEn |
| | 0 | 218 | | Changed |= ChangeState.DynamicLengthMetadata; |
| | 0 | 219 | | } |
| | | 220 | | } |
| | | 221 | | |
| | | 222 | | /// <summary> |
| | | 223 | | /// The collection of entries that are currently in the ZipArchive. This may not accurately represent the actual |
| | | 224 | | /// </summary> |
| | | 225 | | /// <exception cref="NotSupportedException">The ZipArchive does not support reading.</exception> |
| | | 226 | | /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> |
| | | 227 | | /// <exception cref="InvalidDataException">The Zip archive is corrupt and the entries cannot be retrieved.</exce |
| | | 228 | | public ReadOnlyCollection<ZipArchiveEntry> Entries |
| | | 229 | | { |
| | | 230 | | get |
| | 1647 | 231 | | { |
| | 1647 | 232 | | if (_mode == ZipArchiveMode.Create) |
| | 0 | 233 | | { |
| | 0 | 234 | | throw new NotSupportedException(SR.EntriesInCreateMode); |
| | | 235 | | } |
| | | 236 | | |
| | 1647 | 237 | | ThrowIfDisposed(); |
| | | 238 | | |
| | 1647 | 239 | | EnsureCentralDirectoryRead(); |
| | 84 | 240 | | return _entriesCollection; |
| | 84 | 241 | | } |
| | | 242 | | } |
| | | 243 | | |
| | | 244 | | /// <summary> |
| | | 245 | | /// The ZipArchiveMode that the ZipArchive was initialized with. |
| | | 246 | | /// </summary> |
| | | 247 | | public ZipArchiveMode Mode |
| | | 248 | | { |
| | | 249 | | get |
| | 3294 | 250 | | { |
| | 3294 | 251 | | return _mode; |
| | 3294 | 252 | | } |
| | | 253 | | } |
| | | 254 | | |
| | | 255 | | /// <summary> |
| | | 256 | | /// Creates an empty entry in the Zip archive with the specified entry name. |
| | | 257 | | /// There are no restrictions on the names of entries. |
| | | 258 | | /// The last write time of the entry is set to the current time. |
| | | 259 | | /// If an entry with the specified name already exists in the archive, a second entry will be created that has a |
| | | 260 | | /// Since no <code>CompressionLevel</code> is specified, the default provided by the implementation of the under |
| | | 261 | | /// algorithm will be used; the <code>ZipArchive</code> will not impose its own default. |
| | | 262 | | /// (Currently, the underlying compression algorithm is provided by the <code>System.IO.Compression.DeflateStrea |
| | | 263 | | /// </summary> |
| | | 264 | | /// <exception cref="ArgumentException">entryName is a zero-length string.</exception> |
| | | 265 | | /// <exception cref="ArgumentNullException">entryName is null.</exception> |
| | | 266 | | /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception> |
| | | 267 | | /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> |
| | | 268 | | /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c |
| | | 269 | | /// <returns>A wrapper for the newly created file entry in the archive.</returns> |
| | | 270 | | public ZipArchiveEntry CreateEntry(string entryName) |
| | 0 | 271 | | { |
| | 0 | 272 | | return DoCreateEntry(entryName, null); |
| | 0 | 273 | | } |
| | | 274 | | |
| | | 275 | | /// <summary> |
| | | 276 | | /// Creates an empty entry in the Zip archive with the specified entry name. There are no restrictions on the na |
| | | 277 | | /// </summary> |
| | | 278 | | /// <exception cref="ArgumentException">entryName is a zero-length string.</exception> |
| | | 279 | | /// <exception cref="ArgumentNullException">entryName is null.</exception> |
| | | 280 | | /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception> |
| | | 281 | | /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> |
| | | 282 | | /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c |
| | | 283 | | /// <param name="compressionLevel">The level of the compression (speed/memory vs. compressed size trade-off).</p |
| | | 284 | | /// <returns>A wrapper for the newly created file entry in the archive.</returns> |
| | | 285 | | public ZipArchiveEntry CreateEntry(string entryName, CompressionLevel compressionLevel) |
| | 0 | 286 | | { |
| | 0 | 287 | | return DoCreateEntry(entryName, compressionLevel); |
| | 0 | 288 | | } |
| | | 289 | | |
| | | 290 | | /// <summary> |
| | | 291 | | /// Creates an empty encrypted entry in the Zip archive with the specified entry name. |
| | | 292 | | /// The encryption key material is derived from the password and stored on the entry |
| | | 293 | | /// so that a subsequent call to <see cref="ZipArchiveEntry.Open()"/> produces an encrypted stream. |
| | | 294 | | /// </summary> |
| | | 295 | | /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c |
| | | 296 | | /// <param name="password">The password to use for encrypting the entry.</param> |
| | | 297 | | /// <param name="encryptionMethod">The encryption method to use.</param> |
| | | 298 | | /// <returns>A wrapper for the newly created file entry in the archive.</returns> |
| | | 299 | | /// <exception cref="ArgumentException"><paramref name="entryName"/> is a zero-length string.</exception> |
| | | 300 | | /// <exception cref="ArgumentNullException"><paramref name="entryName"/> is <see langword="null"/>.</exception> |
| | | 301 | | /// <exception cref="ArgumentException"><paramref name="password"/> is empty.</exception> |
| | | 302 | | /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception> |
| | | 303 | | /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> |
| | | 304 | | public ZipArchiveEntry CreateEntry(string entryName, ReadOnlySpan<char> password, ZipEncryptionMethod encryption |
| | 0 | 305 | | { |
| | 0 | 306 | | ZipArchiveEntry entry = DoCreateEntry(entryName, null); |
| | 0 | 307 | | entry.PrepareEncryption(password, encryptionMethod); |
| | | 308 | | |
| | 0 | 309 | | return entry; |
| | 0 | 310 | | } |
| | | 311 | | |
| | | 312 | | /// <summary> |
| | | 313 | | /// Creates an empty encrypted entry in the Zip archive with the specified entry name and compression level. |
| | | 314 | | /// The encryption key material is derived from the password and stored on the entry |
| | | 315 | | /// so that a subsequent call to <see cref="ZipArchiveEntry.Open()"/> produces an encrypted stream. |
| | | 316 | | /// </summary> |
| | | 317 | | /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c |
| | | 318 | | /// <param name="compressionLevel">The level of the compression (speed/memory vs. compressed size trade-off).</p |
| | | 319 | | /// <param name="password">The password to use for encrypting the entry.</param> |
| | | 320 | | /// <param name="encryptionMethod">The encryption method to use.</param> |
| | | 321 | | /// <returns>A wrapper for the newly created file entry in the archive.</returns> |
| | | 322 | | /// <exception cref="ArgumentException"><paramref name="entryName"/> is a zero-length string.</exception> |
| | | 323 | | /// <exception cref="ArgumentNullException"><paramref name="entryName"/> is <see langword="null"/>.</exception> |
| | | 324 | | /// <exception cref="ArgumentException"><paramref name="password"/> is empty.</exception> |
| | | 325 | | /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception> |
| | | 326 | | /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> |
| | | 327 | | public ZipArchiveEntry CreateEntry(string entryName, CompressionLevel compressionLevel, ReadOnlySpan<char> passw |
| | 0 | 328 | | { |
| | 0 | 329 | | ZipArchiveEntry entry = DoCreateEntry(entryName, compressionLevel); |
| | 0 | 330 | | entry.PrepareEncryption(password, encryptionMethod); |
| | | 331 | | |
| | 0 | 332 | | return entry; |
| | 0 | 333 | | } |
| | | 334 | | |
| | | 335 | | /// <summary> |
| | | 336 | | /// Releases the unmanaged resources used by ZipArchive and optionally finishes writing the archive and releases |
| | | 337 | | /// </summary> |
| | | 338 | | /// <param name="disposing">true to finish writing the archive and release unmanaged and managed resources, fals |
| | | 339 | | protected virtual void Dispose(bool disposing) |
| | 42 | 340 | | { |
| | 42 | 341 | | if (disposing && !_isDisposed) |
| | 42 | 342 | | { |
| | | 343 | | try |
| | 42 | 344 | | { |
| | 42 | 345 | | switch (_mode) |
| | | 346 | | { |
| | | 347 | | case ZipArchiveMode.Read: |
| | 42 | 348 | | break; |
| | | 349 | | case ZipArchiveMode.Create: |
| | 0 | 350 | | WriteFile(); |
| | 0 | 351 | | break; |
| | | 352 | | case ZipArchiveMode.Update: |
| | | 353 | | default: |
| | 0 | 354 | | Debug.Assert(_mode == ZipArchiveMode.Update); |
| | | 355 | | // Only write if the archive has been modified |
| | 0 | 356 | | if (IsModified) |
| | 0 | 357 | | { |
| | 0 | 358 | | WriteFile(); |
| | 0 | 359 | | } |
| | | 360 | | else |
| | 0 | 361 | | { |
| | | 362 | | // Even if we didn't write, unload any entry buffers that may have been loaded |
| | 0 | 363 | | foreach (ZipArchiveEntry entry in _entries) |
| | 0 | 364 | | { |
| | 0 | 365 | | entry.UnloadStreams(); |
| | 0 | 366 | | } |
| | 0 | 367 | | } |
| | 0 | 368 | | break; |
| | | 369 | | } |
| | 42 | 370 | | } |
| | | 371 | | finally |
| | 42 | 372 | | { |
| | 42 | 373 | | CloseStreams(); |
| | 42 | 374 | | _isDisposed = true; |
| | 42 | 375 | | } |
| | 42 | 376 | | } |
| | 42 | 377 | | } |
| | | 378 | | /// <summary> |
| | | 379 | | /// Finishes writing the archive and releases all resources used by the ZipArchive object, unless the object was |
| | | 380 | | /// </summary> |
| | 42 | 381 | | public void Dispose() => Dispose(true); |
| | | 382 | | |
| | | 383 | | /// <summary> |
| | | 384 | | /// Retrieves a wrapper for the file entry in the archive with the specified name. Names are compared using ordi |
| | | 385 | | /// </summary> |
| | | 386 | | /// <exception cref="ArgumentException">entryName is a zero-length string.</exception> |
| | | 387 | | /// <exception cref="ArgumentNullException">entryName is null.</exception> |
| | | 388 | | /// <exception cref="NotSupportedException">The ZipArchive does not support reading.</exception> |
| | | 389 | | /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception> |
| | | 390 | | /// <exception cref="InvalidDataException">The Zip archive is corrupt and the entries cannot be retrieved.</exce |
| | | 391 | | /// <param name="entryName">A path relative to the root of the archive, identifying the desired entry.</param> |
| | | 392 | | /// <returns>A wrapper for the file entry in the archive. If no entry in the archive exists with the specified n |
| | | 393 | | public ZipArchiveEntry? GetEntry(string entryName) |
| | 0 | 394 | | { |
| | 0 | 395 | | ArgumentNullException.ThrowIfNull(entryName); |
| | | 396 | | |
| | 0 | 397 | | if (_mode == ZipArchiveMode.Create) |
| | 0 | 398 | | { |
| | 0 | 399 | | throw new NotSupportedException(SR.EntriesInCreateMode); |
| | | 400 | | } |
| | | 401 | | |
| | 0 | 402 | | EnsureCentralDirectoryRead(); |
| | 0 | 403 | | _entriesDictionary.TryGetValue(entryName, out ZipArchiveEntry? result); |
| | 0 | 404 | | return result; |
| | 0 | 405 | | } |
| | | 406 | | |
| | 4420 | 407 | | internal Stream ArchiveStream => _archiveStream; |
| | | 408 | | |
| | 0 | 409 | | internal uint NumberOfThisDisk => _numberOfThisDisk; |
| | | 410 | | |
| | | 411 | | internal Encoding? EntryNameAndCommentEncoding |
| | | 412 | | { |
| | 29368 | 413 | | get => _entryNameAndCommentEncoding; |
| | | 414 | | |
| | | 415 | | private set |
| | 3476 | 416 | | { |
| | | 417 | | // value == null is fine. This means the user does not want to overwrite default encoding picking logic. |
| | | 418 | | |
| | | 419 | | // The Zip file spec [http://www.pkware.com/documents/casestudies/APPNOTE.TXT] specifies a bit in the en |
| | | 420 | | // (specifically: the language encoding flag (EFS) in the general purpose bit flag of the local file hea |
| | | 421 | | // basically says: UTF8 (1) or CP437 (0). But in reality, tools replace CP437 with "something else that |
| | | 422 | | // For instance, the Windows Shell Zip tool takes "something else" to mean "the local system codepage". |
| | | 423 | | // We default to the same behaviour, but we let the user explicitly specify the encoding to use for case |
| | | 424 | | // understand their use case well enough. |
| | | 425 | | // Since the definition of acceptable encodings for the "something else" case is in reality by conventio |
| | | 426 | | // immediately clear, whether non-UTF8 Unicode encodings are acceptable. To determine that we would need |
| | | 427 | | // what is currently being done in the field, but we do not have the time for it right now. |
| | | 428 | | // So, we artificially disallow non-UTF8 Unicode encodings for now to make sure we are not creating a co |
| | | 429 | | // for something other tools do not support. If we realise in future that "something else" should includ |
| | | 430 | | // Unicode encodings, we can remove this restriction. |
| | | 431 | | |
| | 3476 | 432 | | if (value != null && |
| | 3476 | 433 | | (value.Equals(Encoding.BigEndianUnicode) |
| | 3476 | 434 | | || value.Equals(Encoding.Unicode))) |
| | 0 | 435 | | { |
| | 0 | 436 | | throw new ArgumentException(SR.EntryNameAndCommentEncodingNotSupported, nameof(EntryNameAndCommentEn |
| | | 437 | | } |
| | | 438 | | |
| | 3476 | 439 | | _entryNameAndCommentEncoding = value; |
| | 3476 | 440 | | } |
| | | 441 | | } |
| | | 442 | | |
| | | 443 | | // This property's value only relates to the top-level fields of the archive (such as the archive comment.) |
| | | 444 | | // New entries in the archive won't change its state. |
| | 3476 | 445 | | internal ChangeState Changed { get; private set; } |
| | | 446 | | |
| | | 447 | | /// <summary> |
| | | 448 | | /// Determines whether the archive has been modified and needs to be written. |
| | | 449 | | /// </summary> |
| | | 450 | | private bool IsModified |
| | | 451 | | { |
| | | 452 | | get |
| | 0 | 453 | | { |
| | | 454 | | // A new archive (created on empty stream) always needs to write the structure |
| | 0 | 455 | | if (_archiveStream.Length == 0) |
| | 0 | 456 | | { |
| | 0 | 457 | | return true; |
| | | 458 | | } |
| | | 459 | | // Archive-level changes (e.g., comment) |
| | 0 | 460 | | if (Changed != ChangeState.Unchanged) |
| | 0 | 461 | | { |
| | 0 | 462 | | return true; |
| | | 463 | | } |
| | | 464 | | // Any deleted entries |
| | 0 | 465 | | if (_firstDeletedEntryOffset != long.MaxValue) |
| | 0 | 466 | | { |
| | 0 | 467 | | return true; |
| | | 468 | | } |
| | | 469 | | // Check if any entry was modified or added |
| | 0 | 470 | | foreach (ZipArchiveEntry entry in _entries) |
| | 0 | 471 | | { |
| | 0 | 472 | | if (!entry.OriginallyInArchive || entry.Changes != ChangeState.Unchanged) |
| | 0 | 473 | | { |
| | 0 | 474 | | return true; |
| | | 475 | | } |
| | 0 | 476 | | } |
| | | 477 | | |
| | 0 | 478 | | return false; |
| | 0 | 479 | | } |
| | | 480 | | } |
| | | 481 | | |
| | | 482 | | private ZipArchiveEntry DoCreateEntry(string entryName, CompressionLevel? compressionLevel) |
| | 0 | 483 | | { |
| | 0 | 484 | | ArgumentException.ThrowIfNullOrEmpty(entryName); |
| | | 485 | | |
| | 0 | 486 | | if (_mode == ZipArchiveMode.Read) |
| | 0 | 487 | | { |
| | 0 | 488 | | throw new NotSupportedException(SR.CreateInReadMode); |
| | | 489 | | } |
| | | 490 | | |
| | 0 | 491 | | ThrowIfDisposed(); |
| | | 492 | | |
| | | 493 | | |
| | 0 | 494 | | ZipArchiveEntry entry = compressionLevel.HasValue ? |
| | 0 | 495 | | new ZipArchiveEntry(this, entryName, compressionLevel.Value) : |
| | 0 | 496 | | new ZipArchiveEntry(this, entryName); |
| | | 497 | | |
| | 0 | 498 | | AddEntry(entry); |
| | | 499 | | |
| | 0 | 500 | | return entry; |
| | 0 | 501 | | } |
| | | 502 | | |
| | | 503 | | internal void AcquireArchiveStream(ZipArchiveEntry entry) |
| | 0 | 504 | | { |
| | | 505 | | // if a previous entry had held the stream but never wrote anything, we write their local header for them |
| | 0 | 506 | | if (_archiveStreamOwner != null) |
| | 0 | 507 | | { |
| | 0 | 508 | | if (!_archiveStreamOwner.EverOpenedForWrite) |
| | 0 | 509 | | { |
| | 0 | 510 | | _archiveStreamOwner.WriteAndFinishLocalEntry(forceWrite: true); |
| | 0 | 511 | | } |
| | | 512 | | else |
| | 0 | 513 | | { |
| | 0 | 514 | | throw new IOException(SR.CreateModeCreateEntryWhileOpen); |
| | | 515 | | } |
| | 0 | 516 | | } |
| | | 517 | | |
| | 0 | 518 | | _archiveStreamOwner = entry; |
| | 0 | 519 | | } |
| | | 520 | | |
| | | 521 | | private void AddEntry(ZipArchiveEntry entry) |
| | 29756 | 522 | | { |
| | 29756 | 523 | | _entries.Add(entry); |
| | 29756 | 524 | | _entriesDictionary.TryAdd(entry.FullName, entry); |
| | 29756 | 525 | | } |
| | | 526 | | |
| | | 527 | | [Conditional("DEBUG")] |
| | 0 | 528 | | internal void DebugAssertIsStillArchiveStreamOwner(ZipArchiveEntry entry) => Debug.Assert(_archiveStreamOwner == |
| | | 529 | | |
| | | 530 | | internal void ReleaseArchiveStream(ZipArchiveEntry entry) |
| | 0 | 531 | | { |
| | 0 | 532 | | Debug.Assert(_archiveStreamOwner == entry); |
| | | 533 | | |
| | 0 | 534 | | _archiveStreamOwner = null; |
| | 0 | 535 | | } |
| | | 536 | | |
| | | 537 | | internal void RemoveEntry(ZipArchiveEntry entry) |
| | 0 | 538 | | { |
| | 0 | 539 | | _entries.Remove(entry); |
| | | 540 | | |
| | 0 | 541 | | _entriesDictionary.Remove(entry.FullName); |
| | | 542 | | // Keep track of the offset of the earliest deleted entry in the archive |
| | 0 | 543 | | if (entry.OriginallyInArchive && entry.OffsetOfLocalHeader < _firstDeletedEntryOffset) |
| | 0 | 544 | | { |
| | 0 | 545 | | _firstDeletedEntryOffset = entry.OffsetOfLocalHeader; |
| | 0 | 546 | | } |
| | 0 | 547 | | } |
| | | 548 | | |
| | | 549 | | internal void ThrowIfDisposed() |
| | 1647 | 550 | | { |
| | 1647 | 551 | | ObjectDisposedException.ThrowIf(_isDisposed, this); |
| | 1647 | 552 | | } |
| | | 553 | | |
| | | 554 | | private void CloseStreams() |
| | 42 | 555 | | { |
| | 42 | 556 | | if (!_leaveOpen) |
| | 42 | 557 | | { |
| | 42 | 558 | | _archiveStream.Dispose(); |
| | 42 | 559 | | _backingStream?.Dispose(); |
| | 42 | 560 | | } |
| | | 561 | | else |
| | 0 | 562 | | { |
| | | 563 | | // if _backingStream isn't null, that means we assigned the original stream they passed |
| | | 564 | | // us to _backingStream (which they requested we leave open), and _archiveStream was |
| | | 565 | | // the temporary copy that we needed |
| | 0 | 566 | | if (_backingStream != null) |
| | 0 | 567 | | { |
| | 0 | 568 | | _archiveStream.Dispose(); |
| | 0 | 569 | | } |
| | 0 | 570 | | } |
| | 42 | 571 | | } |
| | | 572 | | |
| | | 573 | | private void EnsureCentralDirectoryRead() |
| | 1647 | 574 | | { |
| | 1647 | 575 | | if (!_readEntries) |
| | 1605 | 576 | | { |
| | 1605 | 577 | | ReadCentralDirectory(); |
| | 42 | 578 | | _readEntries = true; |
| | 42 | 579 | | } |
| | 84 | 580 | | } |
| | | 581 | | |
| | | 582 | | private void ReadCentralDirectoryInitialize(out long numberOfEntries, out bool saveExtraFieldsAndComments, out b |
| | 3210 | 583 | | { |
| | | 584 | | // assume ReadEndOfCentralDirectory has been called and has populated _centralDirectoryStart |
| | | 585 | | |
| | 3210 | 586 | | _archiveStream.Seek(_centralDirectoryStart, SeekOrigin.Begin); |
| | | 587 | | |
| | 3210 | 588 | | numberOfEntries = 0; |
| | 3210 | 589 | | saveExtraFieldsAndComments = Mode == ZipArchiveMode.Update; |
| | | 590 | | |
| | 3210 | 591 | | continueReadingCentralDirectory = true; |
| | | 592 | | // total bytes read from central directory |
| | 3210 | 593 | | bytesRead = 0; |
| | | 594 | | // current position in the current buffer |
| | 3210 | 595 | | currPosition = 0; |
| | | 596 | | // total bytes read from all file headers starting in the current buffer |
| | 3210 | 597 | | bytesConsumed = 0; |
| | | 598 | | |
| | 3210 | 599 | | _entries.Clear(); |
| | 3210 | 600 | | _entriesDictionary.Clear(); |
| | 3210 | 601 | | } |
| | | 602 | | |
| | | 603 | | private bool ReadCentralDirectoryEndOfInnerLoopWork(bool result, ZipCentralDirectoryFileHeader? currentHeader, i |
| | 30986 | 604 | | { |
| | 30986 | 605 | | if (!result) |
| | 1230 | 606 | | { |
| | 1230 | 607 | | continueReadingCentralDirectory = false; |
| | 1230 | 608 | | return false; |
| | | 609 | | } |
| | | 610 | | |
| | 29756 | 611 | | Debug.Assert(currentHeader != null, "currentHeader should not be null here"); |
| | 29756 | 612 | | AddEntry(new ZipArchiveEntry(this, currentHeader)); |
| | 29756 | 613 | | numberOfEntries++; |
| | 29756 | 614 | | if (numberOfEntries > _expectedNumberOfEntries) |
| | 560 | 615 | | { |
| | 560 | 616 | | throw new InvalidDataException(SR.NumEntriesWrong); |
| | | 617 | | } |
| | | 618 | | |
| | 29196 | 619 | | currPosition += bytesConsumed; |
| | 29196 | 620 | | bytesRead += bytesConsumed; |
| | | 621 | | |
| | 29196 | 622 | | return true; |
| | 30426 | 623 | | } |
| | | 624 | | |
| | | 625 | | private void ReadCentralDirectoryEndOfOuterLoopWork(ref int currPosition, ReadOnlySpan<byte> sizedFileBuffer) |
| | 3792 | 626 | | { |
| | | 627 | | // We've run out of possible space in the entry - seek backwards by the number of bytes remaining in |
| | | 628 | | // this buffer (so that the next buffer overlaps with this one) and retry. |
| | 3792 | 629 | | if (currPosition < sizedFileBuffer.Length) |
| | 3600 | 630 | | { |
| | 3600 | 631 | | _archiveStream.Seek(-(sizedFileBuffer.Length - currPosition), SeekOrigin.Current); |
| | 3600 | 632 | | } |
| | 3792 | 633 | | currPosition = 0; |
| | 3792 | 634 | | } |
| | | 635 | | |
| | | 636 | | private void ReadCentralDirectoryPostOuterLoopWork(long numberOfEntries) |
| | 2524 | 637 | | { |
| | 2524 | 638 | | if (numberOfEntries != _expectedNumberOfEntries) |
| | 2440 | 639 | | { |
| | 2440 | 640 | | throw new InvalidDataException(SR.NumEntriesWrong); |
| | | 641 | | } |
| | | 642 | | |
| | | 643 | | // Sort _entries by each archive entry's position. This supports the algorithm in WriteFile, so is only |
| | | 644 | | // necessary when the ZipArchive has been opened in Update mode. |
| | 84 | 645 | | if (Mode == ZipArchiveMode.Update) |
| | 0 | 646 | | { |
| | 0 | 647 | | _entries.Sort(ZipArchiveEntry.LocalHeaderOffsetComparer.Instance); |
| | | 648 | | |
| | | 649 | | // Precompute EndOfLocalEntryData for each entry. At read time every entry is original |
| | | 650 | | // and sorted by offset, so entry[i]'s end is entry[i+1]'s start (or the central directory |
| | | 651 | | // for the last entry). This correctly includes any trailing data descriptor bytes. |
| | 0 | 652 | | for (int i = 0; i < _entries.Count; i++) |
| | 0 | 653 | | { |
| | 0 | 654 | | _entries[i].EndOfLocalEntryData = i < _entries.Count - 1 |
| | 0 | 655 | | ? _entries[i + 1].OffsetOfLocalHeader |
| | 0 | 656 | | : _centralDirectoryStart; |
| | 0 | 657 | | } |
| | 0 | 658 | | } |
| | 84 | 659 | | } |
| | | 660 | | |
| | | 661 | | private void ReadCentralDirectory() |
| | 1605 | 662 | | { |
| | 1605 | 663 | | byte[] arrayPoolArray = ArrayPool<byte>.Shared.Rent(ReadCentralDirectoryReadBufferSize); |
| | | 664 | | try |
| | 1605 | 665 | | { |
| | 1605 | 666 | | Span<byte> fileBuffer = arrayPoolArray.AsSpan(); |
| | | 667 | | |
| | 1605 | 668 | | ReadCentralDirectoryInitialize(out long numberOfEntries, out bool saveExtraFieldsAndComments, out bool c |
| | | 669 | | |
| | | 670 | | // read the central directory |
| | 3501 | 671 | | while (continueReadingCentralDirectory) |
| | 2239 | 672 | | { |
| | | 673 | | // the buffer read must always be large enough to fit the constant section size of at least one head |
| | 2239 | 674 | | int currBytesRead = _archiveStream.ReadAtLeast(fileBuffer, ZipCentralDirectoryFileHeader.BlockConsta |
| | | 675 | | |
| | 2239 | 676 | | ReadOnlySpan<byte> sizedFileBuffer = fileBuffer.Slice(0, currBytesRead); |
| | 2239 | 677 | | continueReadingCentralDirectory = currBytesRead >= ZipCentralDirectoryFileHeader.BlockConstantSectio |
| | | 678 | | |
| | 16837 | 679 | | while (currPosition + ZipCentralDirectoryFileHeader.BlockConstantSectionSize <= currBytesRead) |
| | 15556 | 680 | | { |
| | 15556 | 681 | | bool result = ZipCentralDirectoryFileHeader.TryReadBlock(sizedFileBuffer.Slice(currPosition), _a |
| | 15556 | 682 | | saveExtraFieldsAndComments, out bytesConsumed, out ZipCentralDirectoryFileHeader? currentHea |
| | | 683 | | |
| | 15493 | 684 | | if (!ReadCentralDirectoryEndOfInnerLoopWork(result, currentHeader, bytesConsumed, ref continueRe |
| | 615 | 685 | | { |
| | 615 | 686 | | break; |
| | | 687 | | } |
| | | 688 | | |
| | 14598 | 689 | | ZipArchiveEntry lastEntry = _entries[_entries.Count - 1]; |
| | 14598 | 690 | | if (lastEntry.IsEncrypted) |
| | 903 | 691 | | { |
| | 903 | 692 | | lastEntry.ReadEncryptionSaltIfNeeded(); |
| | 903 | 693 | | } |
| | 14598 | 694 | | } |
| | | 695 | | |
| | 1896 | 696 | | ReadCentralDirectoryEndOfOuterLoopWork(ref currPosition, sizedFileBuffer); |
| | 1896 | 697 | | } |
| | | 698 | | |
| | 1262 | 699 | | ReadCentralDirectoryPostOuterLoopWork(numberOfEntries); |
| | 42 | 700 | | } |
| | 0 | 701 | | catch (EndOfStreamException ex) |
| | 0 | 702 | | { |
| | 0 | 703 | | throw new InvalidDataException(SR.CentralDirectoryInvalid, ex); |
| | | 704 | | } |
| | | 705 | | finally |
| | 1605 | 706 | | { |
| | 1605 | 707 | | ArrayPool<byte>.Shared.Return(arrayPoolArray); |
| | 1605 | 708 | | } |
| | 42 | 709 | | } |
| | | 710 | | |
| | | 711 | | private void ReadEndOfCentralDirectoryInnerWork(ZipEndOfCentralDirectoryBlock eocd) |
| | 3446 | 712 | | { |
| | 3446 | 713 | | if (eocd.NumberOfThisDisk != eocd.NumberOfTheDiskWithTheStartOfTheCentralDirectory) |
| | 10 | 714 | | { |
| | 10 | 715 | | throw new InvalidDataException(SR.SplitSpanned); |
| | | 716 | | } |
| | | 717 | | |
| | 3436 | 718 | | _numberOfThisDisk = eocd.NumberOfThisDisk; |
| | 3436 | 719 | | _centralDirectoryStart = eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber; |
| | | 720 | | |
| | 3436 | 721 | | if (eocd.NumberOfEntriesInTheCentralDirectory != eocd.NumberOfEntriesInTheCentralDirectoryOnThisDisk) |
| | 6 | 722 | | { |
| | 6 | 723 | | throw new InvalidDataException(SR.SplitSpanned); |
| | | 724 | | } |
| | | 725 | | |
| | 3430 | 726 | | _expectedNumberOfEntries = eocd.NumberOfEntriesInTheCentralDirectory; |
| | | 727 | | |
| | 3430 | 728 | | _archiveComment = eocd.ArchiveComment; |
| | 3430 | 729 | | } |
| | | 730 | | |
| | | 731 | | // This function reads all the EOCD stuff it needs to find the offset to the start of the central directory |
| | | 732 | | // This offset gets put in _centralDirectoryStart and the number of this disk gets put in _numberOfThisDisk |
| | | 733 | | // Also does some verification that this isn't a split/spanned archive |
| | | 734 | | // Also checks that offset to CD isn't out of bounds |
| | | 735 | | private void ReadEndOfCentralDirectory() |
| | 1738 | 736 | | { |
| | | 737 | | try |
| | 1738 | 738 | | { |
| | | 739 | | // This seeks backwards almost to the beginning of the EOCD, one byte after where the signature would be |
| | | 740 | | // located if the EOCD had the minimum possible size (no file zip comment) |
| | 1738 | 741 | | _archiveStream.Seek(-ZipEndOfCentralDirectoryBlock.SizeOfBlockWithoutSignature, SeekOrigin.End); |
| | | 742 | | |
| | | 743 | | // If the EOCD has the minimum possible size (no zip file comment), then exactly the previous 4 bytes wi |
| | | 744 | | // But if the EOCD has max possible size, the signature should be found somewhere in the previous 64K + |
| | 1735 | 745 | | if (!ZipHelper.SeekBackwardsToSignature(_archiveStream, |
| | 1735 | 746 | | ZipEndOfCentralDirectoryBlock.SignatureConstantBytes, |
| | 1735 | 747 | | ZipEndOfCentralDirectoryBlock.ZipFileCommentMaxLength + ZipEndOfCentralDirectoryBlock.FieldLengt |
| | 6 | 748 | | throw new InvalidDataException(SR.EOCDNotFound); |
| | | 749 | | |
| | 1729 | 750 | | long eocdStart = _archiveStream.Position; |
| | | 751 | | |
| | | 752 | | // read the EOCD |
| | 1729 | 753 | | ZipEndOfCentralDirectoryBlock eocd = ZipEndOfCentralDirectoryBlock.ReadBlock(_archiveStream); |
| | | 754 | | |
| | 1723 | 755 | | ReadEndOfCentralDirectoryInnerWork(eocd); |
| | | 756 | | |
| | 1715 | 757 | | TryReadZip64EndOfCentralDirectory(eocd, eocdStart); |
| | | 758 | | |
| | 1614 | 759 | | if (_centralDirectoryStart > _archiveStream.Length) |
| | 9 | 760 | | { |
| | 9 | 761 | | throw new InvalidDataException(SR.FieldTooBigOffsetToCD); |
| | | 762 | | } |
| | 1605 | 763 | | } |
| | 0 | 764 | | catch (EndOfStreamException ex) |
| | 0 | 765 | | { |
| | 0 | 766 | | throw new InvalidDataException(SR.CDCorrupt, ex); |
| | | 767 | | } |
| | 3 | 768 | | catch (IOException ex) |
| | 3 | 769 | | { |
| | 3 | 770 | | throw new InvalidDataException(SR.CDCorrupt, ex); |
| | | 771 | | } |
| | 1605 | 772 | | } |
| | | 773 | | |
| | | 774 | | private void TryReadZip64EndOfCentralDirectoryInnerInitialWork(Zip64EndOfCentralDirectoryLocator? locator) |
| | 178 | 775 | | { |
| | 178 | 776 | | if (locator == null || locator.OffsetOfZip64EOCD > long.MaxValue) |
| | 64 | 777 | | { |
| | 64 | 778 | | throw new InvalidDataException(SR.FieldTooBigOffsetToZip64EOCD); |
| | | 779 | | } |
| | | 780 | | |
| | 114 | 781 | | long zip64EOCDOffset = (long)locator.OffsetOfZip64EOCD; |
| | | 782 | | |
| | 114 | 783 | | if (zip64EOCDOffset < 0 || zip64EOCDOffset > _archiveStream.Length) |
| | 60 | 784 | | { |
| | 60 | 785 | | throw new InvalidDataException(SR.InvalidOffsetToZip64EOCD); |
| | | 786 | | } |
| | | 787 | | |
| | 54 | 788 | | _archiveStream.Seek(zip64EOCDOffset, SeekOrigin.Begin); |
| | 54 | 789 | | } |
| | | 790 | | |
| | | 791 | | private void TryReadZip64EndOfCentralDirectoryInnerFinalWork(Zip64EndOfCentralDirectoryRecord record) |
| | 36 | 792 | | { |
| | 36 | 793 | | _numberOfThisDisk = record.NumberOfThisDisk; |
| | | 794 | | |
| | 36 | 795 | | if (record.NumberOfEntriesTotal > long.MaxValue) |
| | 18 | 796 | | { |
| | 18 | 797 | | throw new InvalidDataException(SR.FieldTooBigNumEntries); |
| | | 798 | | } |
| | | 799 | | |
| | 18 | 800 | | if (record.OffsetOfCentralDirectory > long.MaxValue) |
| | 14 | 801 | | { |
| | 14 | 802 | | throw new InvalidDataException(SR.FieldTooBigOffsetToCD); |
| | | 803 | | } |
| | | 804 | | |
| | 4 | 805 | | if (record.NumberOfEntriesTotal != record.NumberOfEntriesOnThisDisk) |
| | 4 | 806 | | { |
| | 4 | 807 | | throw new InvalidDataException(SR.SplitSpanned); |
| | | 808 | | } |
| | | 809 | | |
| | 0 | 810 | | _expectedNumberOfEntries = (long)record.NumberOfEntriesTotal; |
| | 0 | 811 | | _centralDirectoryStart = (long)record.OffsetOfCentralDirectory; |
| | 0 | 812 | | } |
| | | 813 | | |
| | | 814 | | // Tries to find the Zip64 End of Central Directory Locator, then the Zip64 End of Central Directory, assuming t |
| | | 815 | | // End of Central Directory block has already been found, as well as the location in the stream where the EOCD s |
| | | 816 | | private void TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eocd, long eocdStart) |
| | 1715 | 817 | | { |
| | | 818 | | // Only bother looking for the Zip64-EOCD stuff if we suspect it is needed because some value is FFFFFFFFF |
| | | 819 | | // because these are the only two values we need, we only worry about these |
| | | 820 | | // if we don't find the Zip64-EOCD, we just give up and try to use the original values |
| | 1715 | 821 | | if (eocd.NumberOfThisDisk == ZipHelper.Mask16Bit || |
| | 1715 | 822 | | eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber == ZipHelper.Mask32Bit || |
| | 1715 | 823 | | eocd.NumberOfEntriesInTheCentralDirectory == ZipHelper.Mask16Bit) |
| | 1292 | 824 | | { |
| | | 825 | | // Read Zip64 End of Central Directory Locator |
| | | 826 | | |
| | | 827 | | // Check if there's enough space before the EOCD to look for the Zip64 EOCDL |
| | 1292 | 828 | | if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize) |
| | 12 | 829 | | { |
| | 12 | 830 | | throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected); |
| | | 831 | | } |
| | | 832 | | |
| | | 833 | | // This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature wo |
| | 1280 | 834 | | _archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigi |
| | | 835 | | |
| | | 836 | | // Exactly the previous 4 bytes should contain the Zip64-EOCDL signature |
| | | 837 | | // if we don't find it, assume it doesn't exist and use data from normal EOCD |
| | 1280 | 838 | | if (ZipHelper.SeekBackwardsToSignature(_archiveStream, |
| | 1280 | 839 | | Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes, |
| | 1280 | 840 | | Zip64EndOfCentralDirectoryLocator.FieldLengths.Signature)) |
| | 89 | 841 | | { |
| | | 842 | | // use locator to get to Zip64-EOCD |
| | 89 | 843 | | Zip64EndOfCentralDirectoryLocator locator = Zip64EndOfCentralDirectoryLocator.TryReadBlock(_archiveS |
| | 89 | 844 | | TryReadZip64EndOfCentralDirectoryInnerInitialWork(locator); |
| | | 845 | | |
| | | 846 | | // Read Zip64 End of Central Directory Record |
| | 27 | 847 | | Zip64EndOfCentralDirectoryRecord record = Zip64EndOfCentralDirectoryRecord.TryReadBlock(_archiveStre |
| | 18 | 848 | | TryReadZip64EndOfCentralDirectoryInnerFinalWork(record); |
| | 0 | 849 | | } |
| | 1191 | 850 | | } |
| | 1614 | 851 | | } |
| | | 852 | | |
| | | 853 | | private static void WriteFileCalculateOffsets(ZipArchiveEntry entry, ref long startingOffset, ref long nextFileO |
| | 0 | 854 | | { |
| | 0 | 855 | | if (entry.Changes == ChangeState.Unchanged) |
| | 0 | 856 | | { |
| | | 857 | | // Keep track of the expected position of the file entry after the final untouched file entry so that wh |
| | | 858 | | // we'll know which position to start writing new entries from. |
| | | 859 | | // EndOfLocalEntryData includes any trailing data descriptor because it was pre-computed from the |
| | | 860 | | // next entry's offset or the central directory start. |
| | 0 | 861 | | nextFileOffset = Math.Max(nextFileOffset, entry.EndOfLocalEntryData); |
| | 0 | 862 | | } |
| | | 863 | | // When calculating the starting offset to load the files from, only look at changed entries which are alrea |
| | | 864 | | else |
| | 0 | 865 | | { |
| | 0 | 866 | | startingOffset = Math.Min(startingOffset, entry.OffsetOfLocalHeader); |
| | 0 | 867 | | } |
| | 0 | 868 | | } |
| | | 869 | | |
| | | 870 | | private static void WriteFileCheckStartingOffset(ZipArchiveEntry entry, ref long completeRewriteStartingOffset) |
| | 0 | 871 | | { |
| | | 872 | | // If the pending data to write is fixed-length metadata in the header, there's no need to load the compress |
| | | 873 | | // We always need to load the local file header's metadata though - at this point, this entry will be writte |
| | | 874 | | // want to make sure that we preserve that metadata. |
| | 0 | 875 | | if ((entry.Changes & (ChangeState.DynamicLengthMetadata | ChangeState.StoredData)) != 0) |
| | 0 | 876 | | { |
| | 0 | 877 | | completeRewriteStartingOffset = Math.Min(completeRewriteStartingOffset, entry.OffsetOfLocalHeader); |
| | 0 | 878 | | } |
| | 0 | 879 | | } |
| | | 880 | | |
| | | 881 | | private void WriteFileUpdateModeFinalWork(long startingOffset, long nextFileOffset) |
| | 0 | 882 | | { |
| | | 883 | | // If the offset of entries to write from is still at long.MaxValue, then we know that nothing has been dele |
| | | 884 | | // nothing has been modified - so we just want to move to the end of all remaining files in the archive. |
| | 0 | 885 | | if (startingOffset == long.MaxValue) |
| | 0 | 886 | | { |
| | 0 | 887 | | startingOffset = nextFileOffset; |
| | 0 | 888 | | } |
| | | 889 | | |
| | 0 | 890 | | _archiveStream.Seek(startingOffset, SeekOrigin.Begin); |
| | 0 | 891 | | } |
| | | 892 | | |
| | | 893 | | private void WriteFileFinalWork() |
| | 0 | 894 | | { |
| | | 895 | | // If entries have been removed and new (smaller) ones added, there could be empty space at the end of the f |
| | | 896 | | // Shrink the file to reclaim this space. |
| | 0 | 897 | | if (_mode == ZipArchiveMode.Update && _archiveStream.Position != _archiveStream.Length) |
| | 0 | 898 | | { |
| | 0 | 899 | | _archiveStream.SetLength(_archiveStream.Position); |
| | 0 | 900 | | } |
| | 0 | 901 | | } |
| | | 902 | | |
| | | 903 | | private void WriteFile() |
| | 0 | 904 | | { |
| | | 905 | | // if we are in create mode, we always set readEntries to true in Init |
| | | 906 | | // if we are in update mode, we call EnsureCentralDirectoryRead, which sets readEntries to true |
| | 0 | 907 | | Debug.Assert(_readEntries); |
| | | 908 | | |
| | | 909 | | // Entries starting after this offset have had a dynamically-sized change. Everything on or after this point |
| | 0 | 910 | | long completeRewriteStartingOffset = 0; |
| | 0 | 911 | | List<ZipArchiveEntry> entriesToWrite = _entries; |
| | | 912 | | |
| | 0 | 913 | | if (_mode == ZipArchiveMode.Update) |
| | 0 | 914 | | { |
| | | 915 | | // Entries starting after this offset have some kind of change made to them. It might just be a fixed-le |
| | | 916 | | // that single entry's metadata can be rewritten without impacting anything else. |
| | 0 | 917 | | long startingOffset = _firstDeletedEntryOffset; |
| | 0 | 918 | | long nextFileOffset = 0; |
| | 0 | 919 | | completeRewriteStartingOffset = startingOffset; |
| | 0 | 920 | | entriesToWrite = new(_entries.Count); |
| | | 921 | | |
| | 0 | 922 | | foreach (ZipArchiveEntry entry in _entries) |
| | 0 | 923 | | { |
| | 0 | 924 | | if (!entry.OriginallyInArchive) |
| | 0 | 925 | | { |
| | 0 | 926 | | entriesToWrite.Add(entry); |
| | 0 | 927 | | } |
| | | 928 | | else |
| | 0 | 929 | | { |
| | 0 | 930 | | WriteFileCalculateOffsets(entry, ref startingOffset, ref nextFileOffset); |
| | | 931 | | |
| | | 932 | | // We want to re-write entries which are after the starting offset of the first entry which has |
| | | 933 | | // NB: the existing ZipArchiveEntries are sorted in _entries by their position ascending. |
| | 0 | 934 | | if (entry.OffsetOfLocalHeader >= startingOffset) |
| | 0 | 935 | | { |
| | 0 | 936 | | WriteFileCheckStartingOffset(entry, ref completeRewriteStartingOffset); |
| | | 937 | | |
| | 0 | 938 | | entry.LoadLocalHeaderExtraFieldIfNeeded(); |
| | 0 | 939 | | if (entry.OffsetOfLocalHeader >= completeRewriteStartingOffset) |
| | 0 | 940 | | { |
| | 0 | 941 | | entry.LoadCompressedBytesIfNeeded(); |
| | 0 | 942 | | } |
| | | 943 | | |
| | 0 | 944 | | entriesToWrite.Add(entry); |
| | 0 | 945 | | } |
| | 0 | 946 | | } |
| | 0 | 947 | | } |
| | 0 | 948 | | WriteFileUpdateModeFinalWork(startingOffset, nextFileOffset); |
| | 0 | 949 | | } |
| | | 950 | | |
| | 0 | 951 | | foreach (ZipArchiveEntry entry in entriesToWrite) |
| | 0 | 952 | | { |
| | | 953 | | // We don't always need to write the local header entry, ZipArchiveEntry is usually able to work out whe |
| | | 954 | | // We want to force this header entry to be written (even for completely untouched entries) if the entry |
| | | 955 | | // which had a pending dynamically-sized write. |
| | 0 | 956 | | bool forceWriteLocalEntry = !entry.OriginallyInArchive || (entry.OriginallyInArchive && entry.OffsetOfLo |
| | | 957 | | |
| | 0 | 958 | | entry.WriteAndFinishLocalEntry(forceWriteLocalEntry); |
| | 0 | 959 | | } |
| | | 960 | | |
| | 0 | 961 | | long plannedCentralDirectoryPosition = _archiveStream.Position; |
| | | 962 | | // If there are no entries in the archive, we still want to create the archive epilogue. |
| | 0 | 963 | | bool archiveEpilogueRequiresUpdate = _entries.Count == 0; |
| | | 964 | | |
| | 0 | 965 | | foreach (ZipArchiveEntry entry in _entries) |
| | 0 | 966 | | { |
| | | 967 | | // The central directory needs to be rewritten if its position has moved, if there's a new entry in the |
| | 0 | 968 | | bool centralDirectoryEntryRequiresUpdate = plannedCentralDirectoryPosition != _centralDirectoryStart |
| | 0 | 969 | | || !entry.OriginallyInArchive || entry.OffsetOfLocalHeader >= completeRewriteStartingOffset; |
| | | 970 | | |
| | 0 | 971 | | entry.WriteCentralDirectoryFileHeader(centralDirectoryEntryRequiresUpdate); |
| | 0 | 972 | | archiveEpilogueRequiresUpdate |= centralDirectoryEntryRequiresUpdate; |
| | 0 | 973 | | } |
| | | 974 | | |
| | 0 | 975 | | long sizeOfCentralDirectory = _archiveStream.Position - plannedCentralDirectoryPosition; |
| | | 976 | | |
| | 0 | 977 | | WriteArchiveEpilogue(plannedCentralDirectoryPosition, sizeOfCentralDirectory, archiveEpilogueRequiresUpdate) |
| | | 978 | | |
| | 0 | 979 | | WriteFileFinalWork(); |
| | 0 | 980 | | } |
| | | 981 | | |
| | | 982 | | private void WriteArchiveEpilogueNoCDChangesWork() |
| | 0 | 983 | | { |
| | 0 | 984 | | _archiveStream.Seek(Zip64EndOfCentralDirectoryRecord.TotalSize, SeekOrigin.Current); |
| | 0 | 985 | | _archiveStream.Seek(Zip64EndOfCentralDirectoryLocator.TotalSize, SeekOrigin.Current); |
| | 0 | 986 | | } |
| | | 987 | | |
| | | 988 | | // writes eocd, and if needed, zip 64 eocd, zip64 eocd locator |
| | | 989 | | // should only throw an exception in extremely exceptional cases because it is called from dispose |
| | | 990 | | private void WriteArchiveEpilogue(long startOfCentralDirectory, long sizeOfCentralDirectory, bool centralDirecto |
| | 0 | 991 | | { |
| | | 992 | | // determine if we need Zip 64 |
| | 0 | 993 | | if (startOfCentralDirectory >= uint.MaxValue |
| | 0 | 994 | | || sizeOfCentralDirectory >= uint.MaxValue |
| | 0 | 995 | | || _entries.Count >= ZipHelper.Mask16Bit |
| | 0 | 996 | | #if DEBUG_FORCE_ZIP64 |
| | 0 | 997 | | || _forceZip64 |
| | 0 | 998 | | #endif |
| | 0 | 999 | | ) |
| | 0 | 1000 | | { |
| | | 1001 | | // if we need zip 64, write zip 64 eocd and locator |
| | 0 | 1002 | | long zip64EOCDRecordStart = _archiveStream.Position; |
| | | 1003 | | |
| | 0 | 1004 | | if (centralDirectoryChanged) |
| | 0 | 1005 | | { |
| | 0 | 1006 | | Zip64EndOfCentralDirectoryRecord.WriteBlock(_archiveStream, _entries.Count, startOfCentralDirectory, |
| | 0 | 1007 | | Zip64EndOfCentralDirectoryLocator.WriteBlock(_archiveStream, zip64EOCDRecordStart); |
| | 0 | 1008 | | } |
| | | 1009 | | else |
| | 0 | 1010 | | { |
| | 0 | 1011 | | WriteArchiveEpilogueNoCDChangesWork(); |
| | 0 | 1012 | | } |
| | 0 | 1013 | | } |
| | | 1014 | | |
| | | 1015 | | // write normal eocd |
| | 0 | 1016 | | if (centralDirectoryChanged || (Changed != ChangeState.Unchanged)) |
| | 0 | 1017 | | { |
| | 0 | 1018 | | ZipEndOfCentralDirectoryBlock.WriteBlock(_archiveStream, _entries.Count, startOfCentralDirectory, sizeOf |
| | 0 | 1019 | | } |
| | | 1020 | | else |
| | 0 | 1021 | | { |
| | 0 | 1022 | | _archiveStream.Seek(ZipEndOfCentralDirectoryBlock.TotalSize + _archiveComment.Length, SeekOrigin.Current |
| | 0 | 1023 | | } |
| | 0 | 1024 | | } |
| | | 1025 | | |
| | | 1026 | | // Confirms that the specified stream is compatible with the specified mode. |
| | | 1027 | | // Returns a boolean that indicates that further work needs to be done for when |
| | | 1028 | | // the mode is Read and the stream is unseekable. |
| | | 1029 | | private static bool ValidateMode(ZipArchiveMode mode, Stream stream) |
| | 3476 | 1030 | | { |
| | | 1031 | | // check stream against mode |
| | 3476 | 1032 | | bool isReadModeAndUnseekable = false; |
| | | 1033 | | |
| | 3476 | 1034 | | switch (mode) |
| | | 1035 | | { |
| | | 1036 | | case ZipArchiveMode.Create: |
| | 0 | 1037 | | if (!stream.CanWrite) |
| | 0 | 1038 | | { |
| | 0 | 1039 | | throw new ArgumentException(SR.CreateModeCapabilities); |
| | | 1040 | | } |
| | 0 | 1041 | | break; |
| | | 1042 | | case ZipArchiveMode.Read: |
| | 3476 | 1043 | | if (!stream.CanRead) |
| | 0 | 1044 | | { |
| | 0 | 1045 | | throw new ArgumentException(SR.ReadModeCapabilities); |
| | | 1046 | | } |
| | 3476 | 1047 | | if (!stream.CanSeek) |
| | 0 | 1048 | | { |
| | 0 | 1049 | | isReadModeAndUnseekable = true; |
| | 0 | 1050 | | } |
| | 3476 | 1051 | | break; |
| | | 1052 | | case ZipArchiveMode.Update: |
| | 0 | 1053 | | if (!stream.CanRead || !stream.CanWrite || !stream.CanSeek) |
| | 0 | 1054 | | { |
| | 0 | 1055 | | throw new ArgumentException(SR.UpdateModeCapabilities); |
| | | 1056 | | } |
| | 0 | 1057 | | break; |
| | | 1058 | | default: |
| | | 1059 | | // still have to throw this, because stream constructor doesn't do mode argument checks |
| | 0 | 1060 | | throw new ArgumentOutOfRangeException(nameof(mode)); |
| | | 1061 | | } |
| | | 1062 | | |
| | 3476 | 1063 | | return isReadModeAndUnseekable; |
| | 3476 | 1064 | | } |
| | | 1065 | | |
| | | 1066 | | // Depending on mode and stream seekability, we will decide if the archive |
| | | 1067 | | // stream needs to be wrapped or not by another stream to help with writing. |
| | | 1068 | | private static Stream DecideArchiveStream(ZipArchiveMode mode, Stream stream) |
| | 5214 | 1069 | | { |
| | 5214 | 1070 | | ArgumentNullException.ThrowIfNull(stream); |
| | | 1071 | | |
| | 5214 | 1072 | | return mode == ZipArchiveMode.Create && !stream.CanSeek ? |
| | 5214 | 1073 | | new PositionPreservingWriteOnlyStreamWrapper(stream) : |
| | 5214 | 1074 | | stream; |
| | 5214 | 1075 | | } |
| | | 1076 | | |
| | | 1077 | | |
| | | 1078 | | [Flags] |
| | | 1079 | | internal enum ChangeState |
| | | 1080 | | { |
| | | 1081 | | Unchanged = 0x0, |
| | | 1082 | | FixedLengthMetadata = 0x1, |
| | | 1083 | | DynamicLengthMetadata = 0x2, |
| | | 1084 | | StoredData = 0x4 |
| | | 1085 | | } |
| | | 1086 | | } |
| | | 1087 | | } |