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 using System.Collections.Generic; 6 using System.Diagnostics; 7 using System.Diagnostics.CodeAnalysis; 8 using System.Text; 9 10 namespace System.IO.Compression 11 { 12 // The disposable fields that this class owns get disposed when the ZipArchive it belongs to gets disposed 13 [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] 14 public partial class ZipArchiveEntry 15 { 16 private const ushort DefaultVersionToExtract = 10; 17 18 // The maximum index of our buffers, from the maximum index of a byte array 19 private const int MaxSingleBufferSize = 0x7FFFFFC7; 20 21 private ZipArchive _archive; 22 private readonly bool _originallyInArchive; 23 private readonly int _diskNumberStart; 24 private readonly ZipVersionMadeByPlatform _versionMadeByPlatform; 25 private ZipVersionNeededValues _versionMadeBySpecification; 26 private ZipVersionNeededValues _versionToExtract; 27 private BitFlagValues _generalPurposeBitFlag; 28 private CompressionMethodValues _storedCompressionMethod; 29 private DateTimeOffset _lastModified; 30 private long _compressedSize; 31 private long _uncompressedSize; 32 private long _offsetOfLocalHeader; 33 private long? _storedOffsetOfCompressedData; 34 private uint _crc32; 35 // An array of buffers, each a maximum of MaxSingleBufferSize in size 36 private byte[][] _compressedBytes; 37 private MemoryStream _storedUncompressedData; 38 private bool _currentlyOpenForWrite; 39 private bool _everOpenedForWrite; 40 private Stream _outstandingWriteStream; 41 private uint _externalFileAttr; 42 private string _storedEntryName; 43 private byte[] _storedEntryNameBytes; 44 // only apply to update mode 45 private List<ZipGenericExtraField> _cdUnknownExtraFields; 46 private List<ZipGenericExtraField> _lhUnknownExtraFields; 47 private byte[] _fileComment; 48 private CompressionLevel? _compressionLevel; 49 50 // Initializes, attaches it to archive ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd)51 internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd) 52 { 53 _archive = archive; 54 55 _originallyInArchive = true; 56 57 _diskNumberStart = cd.DiskNumberStart; 58 _versionMadeByPlatform = (ZipVersionMadeByPlatform)cd.VersionMadeByCompatibility; 59 _versionMadeBySpecification = (ZipVersionNeededValues)cd.VersionMadeBySpecification; 60 _versionToExtract = (ZipVersionNeededValues)cd.VersionNeededToExtract; 61 _generalPurposeBitFlag = (BitFlagValues)cd.GeneralPurposeBitFlag; 62 CompressionMethod = (CompressionMethodValues)cd.CompressionMethod; 63 _lastModified = new DateTimeOffset(ZipHelper.DosTimeToDateTime(cd.LastModified)); 64 _compressedSize = cd.CompressedSize; 65 _uncompressedSize = cd.UncompressedSize; 66 _externalFileAttr = cd.ExternalFileAttributes; 67 _offsetOfLocalHeader = cd.RelativeOffsetOfLocalHeader; 68 // we don't know this yet: should be _offsetOfLocalHeader + 30 + _storedEntryNameBytes.Length + extrafieldlength 69 // but entryname/extra length could be different in LH 70 _storedOffsetOfCompressedData = null; 71 _crc32 = cd.Crc32; 72 73 _compressedBytes = null; 74 _storedUncompressedData = null; 75 _currentlyOpenForWrite = false; 76 _everOpenedForWrite = false; 77 _outstandingWriteStream = null; 78 79 FullName = DecodeEntryName(cd.Filename); 80 81 _lhUnknownExtraFields = null; 82 // the cd should have these as null if we aren't in Update mode 83 _cdUnknownExtraFields = cd.ExtraFields; 84 _fileComment = cd.FileComment; 85 86 _compressionLevel = null; 87 } 88 89 // Initializes new entry ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel compressionLevel)90 internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel compressionLevel) 91 : this(archive, entryName) 92 { 93 _compressionLevel = compressionLevel; 94 } 95 96 // Initializes new entry ZipArchiveEntry(ZipArchive archive, string entryName)97 internal ZipArchiveEntry(ZipArchive archive, string entryName) 98 { 99 _archive = archive; 100 101 _originallyInArchive = false; 102 103 _diskNumberStart = 0; 104 _versionMadeByPlatform = CurrentZipPlatform; 105 _versionMadeBySpecification = ZipVersionNeededValues.Default; 106 _versionToExtract = ZipVersionNeededValues.Default; // this must happen before following two assignment 107 _generalPurposeBitFlag = 0; 108 CompressionMethod = CompressionMethodValues.Deflate; 109 _lastModified = DateTimeOffset.Now; 110 111 _compressedSize = 0; // we don't know these yet 112 _uncompressedSize = 0; 113 _externalFileAttr = 0; 114 _offsetOfLocalHeader = 0; 115 _storedOffsetOfCompressedData = null; 116 _crc32 = 0; 117 118 _compressedBytes = null; 119 _storedUncompressedData = null; 120 _currentlyOpenForWrite = false; 121 _everOpenedForWrite = false; 122 _outstandingWriteStream = null; 123 124 FullName = entryName; 125 126 _cdUnknownExtraFields = null; 127 _lhUnknownExtraFields = null; 128 _fileComment = null; 129 130 _compressionLevel = null; 131 132 if (_storedEntryNameBytes.Length > ushort.MaxValue) 133 throw new ArgumentException(SR.EntryNamesTooLong); 134 135 // grab the stream if we're in create mode 136 if (_archive.Mode == ZipArchiveMode.Create) 137 { 138 _archive.AcquireArchiveStream(this); 139 } 140 } 141 142 /// <summary> 143 /// The ZipArchive that this entry belongs to. If this entry has been deleted, this will return null. 144 /// </summary> 145 public ZipArchive Archive => _archive; 146 147 [CLSCompliant(false)] 148 public uint Crc32 => _crc32; 149 150 /// <summary> 151 /// The compressed size of the entry. If the archive that the entry belongs to is in Create mode, attempts to get this property will always throw an exception. If the archive that the entry belongs to is in update mode, this property will only be valid if the entry has not been opened. 152 /// </summary> 153 /// <exception cref="InvalidOperationException">This property is not available because the entry has been written to or modified.</exception> 154 public long CompressedLength 155 { 156 get 157 { 158 if (_everOpenedForWrite) 159 throw new InvalidOperationException(SR.LengthAfterWrite); 160 return _compressedSize; 161 } 162 } 163 164 public int ExternalAttributes 165 { 166 get 167 { 168 return (int)_externalFileAttr; 169 } 170 set 171 { 172 ThrowIfInvalidArchive(); 173 _externalFileAttr = (uint)value; 174 } 175 } 176 177 /// <summary> 178 /// The relative path of the entry as stored in the Zip archive. Note that Zip archives allow any string to be the path of the entry, including invalid and absolute paths. 179 /// </summary> 180 public string FullName 181 { 182 get 183 { 184 return _storedEntryName; 185 } 186 187 private set 188 { 189 if (value == null) 190 throw new ArgumentNullException(nameof(FullName)); 191 192 bool isUTF8; 193 _storedEntryNameBytes = EncodeEntryName(value, out isUTF8); 194 _storedEntryName = value; 195 196 if (isUTF8) 197 _generalPurposeBitFlag |= BitFlagValues.UnicodeFileName; 198 else 199 _generalPurposeBitFlag &= ~BitFlagValues.UnicodeFileName; 200 201 if (ParseFileName(value, _versionMadeByPlatform) == "") 202 VersionToExtractAtLeast(ZipVersionNeededValues.ExplicitDirectory); 203 } 204 } 205 206 /// <summary> 207 /// The last write time of the entry as stored in the Zip archive. When setting this property, the DateTime will be converted to the 208 /// Zip timestamp format, which supports a resolution of two seconds. If the data in the last write time field is not a valid Zip timestamp, 209 /// an indicator value of 1980 January 1 at midnight will be returned. 210 /// </summary> 211 /// <exception cref="NotSupportedException">An attempt to set this property was made, but the ZipArchive that this entry belongs to was 212 /// opened in read-only mode.</exception> 213 /// <exception cref="ArgumentOutOfRangeException">An attempt was made to set this property to a value that cannot be represented in the 214 /// Zip timestamp format. The earliest date/time that can be represented is 1980 January 1 0:00:00 (midnight), and the last date/time 215 /// that can be represented is 2107 December 31 23:59:58 (one second before midnight).</exception> 216 public DateTimeOffset LastWriteTime 217 { 218 get 219 { 220 return _lastModified; 221 } 222 set 223 { 224 ThrowIfInvalidArchive(); 225 if (_archive.Mode == ZipArchiveMode.Read) 226 throw new NotSupportedException(SR.ReadOnlyArchive); 227 if (_archive.Mode == ZipArchiveMode.Create && _everOpenedForWrite) 228 throw new IOException(SR.FrozenAfterWrite); 229 if (value.DateTime.Year < ZipHelper.ValidZipDate_YearMin || value.DateTime.Year > ZipHelper.ValidZipDate_YearMax) 230 throw new ArgumentOutOfRangeException(nameof(value), SR.DateTimeOutOfRange); 231 232 _lastModified = value; 233 } 234 } 235 236 /// <summary> 237 /// The uncompressed size of the entry. This property is not valid in Create mode, and it is only valid in Update mode if the entry has not been opened. 238 /// </summary> 239 /// <exception cref="InvalidOperationException">This property is not available because the entry has been written to or modified.</exception> 240 public long Length 241 { 242 get 243 { 244 if (_everOpenedForWrite) 245 throw new InvalidOperationException(SR.LengthAfterWrite); 246 return _uncompressedSize; 247 } 248 } 249 250 /// <summary> 251 /// The filename of the entry. This is equivalent to the substring of Fullname that follows the final directory separator character. 252 /// </summary> 253 public string Name => ParseFileName(FullName, _versionMadeByPlatform); 254 255 /// <summary> 256 /// Deletes the entry from the archive. 257 /// </summary> 258 /// <exception cref="IOException">The entry is already open for reading or writing.</exception> 259 /// <exception cref="NotSupportedException">The ZipArchive that this entry belongs to was opened in a mode other than ZipArchiveMode.Update. </exception> 260 /// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exception> Delete()261 public void Delete() 262 { 263 if (_archive == null) 264 return; 265 266 if (_currentlyOpenForWrite) 267 throw new IOException(SR.DeleteOpenEntry); 268 269 if (_archive.Mode != ZipArchiveMode.Update) 270 throw new NotSupportedException(SR.DeleteOnlyInUpdate); 271 272 _archive.ThrowIfDisposed(); 273 274 _archive.RemoveEntry(this); 275 _archive = null; 276 UnloadStreams(); 277 } 278 279 /// <summary> 280 /// Opens the entry. If the archive that the entry belongs to was opened in Read mode, the returned stream will be readable, and it may or may not be seekable. If Create mode, the returned stream will be writable and not seekable. If Update mode, the returned stream will be readable, writable, seekable, and support SetLength. 281 /// </summary> 282 /// <returns>A Stream that represents the contents of the entry.</returns> 283 /// <exception cref="IOException">The entry is already currently open for writing. -or- The entry has been deleted from the archive. -or- The archive that this entry belongs to was opened in ZipArchiveMode.Create, and this entry has already been written to once.</exception> 284 /// <exception cref="InvalidDataException">The entry is missing from the archive or is corrupt and cannot be read. -or- The entry has been compressed using a compression method that is not supported.</exception> 285 /// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exception> Open()286 public Stream Open() 287 { 288 ThrowIfInvalidArchive(); 289 290 switch (_archive.Mode) 291 { 292 case ZipArchiveMode.Read: 293 return OpenInReadMode(checkOpenable: true); 294 case ZipArchiveMode.Create: 295 return OpenInWriteMode(); 296 case ZipArchiveMode.Update: 297 default: 298 Debug.Assert(_archive.Mode == ZipArchiveMode.Update); 299 return OpenInUpdateMode(); 300 } 301 } 302 303 /// <summary> 304 /// Returns the FullName of the entry. 305 /// </summary> 306 /// <returns>FullName of the entry</returns> ToString()307 public override string ToString() 308 { 309 return FullName; 310 } 311 312 // Only allow opening ZipArchives with large ZipArchiveEntries in update mode when running in a 64-bit process. 313 // This is for compatibility with old behavior that threw an exception for all process bitnesses, because this 314 // will not work in a 32-bit process. 315 private static readonly bool s_allowLargeZipArchiveEntriesInUpdateMode = IntPtr.Size > 4; 316 317 internal bool EverOpenedForWrite => _everOpenedForWrite; 318 319 private long OffsetOfCompressedData 320 { 321 get 322 { 323 if (_storedOffsetOfCompressedData == null) 324 { 325 _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); 326 // by calling this, we are using local header _storedEntryNameBytes.Length and extraFieldLength 327 // to find start of data, but still using central directory size information 328 if (!ZipLocalFileHeader.TrySkipBlock(_archive.ArchiveReader)) 329 throw new InvalidDataException(SR.LocalFileHeaderCorrupt); 330 _storedOffsetOfCompressedData = _archive.ArchiveStream.Position; 331 } 332 return _storedOffsetOfCompressedData.Value; 333 } 334 } 335 336 private MemoryStream UncompressedData 337 { 338 get 339 { 340 if (_storedUncompressedData == null) 341 { 342 // this means we have never opened it before 343 344 // if _uncompressedSize > int.MaxValue, it's still okay, because MemoryStream will just 345 // grow as data is copied into it 346 _storedUncompressedData = new MemoryStream((int)_uncompressedSize); 347 348 if (_originallyInArchive) 349 { 350 using (Stream decompressor = OpenInReadMode(false)) 351 { 352 try 353 { 354 decompressor.CopyTo(_storedUncompressedData); 355 } 356 catch (InvalidDataException) 357 { 358 // this is the case where the archive say the entry is deflate, but deflateStream 359 // throws an InvalidDataException. This property should only be getting accessed in 360 // Update mode, so we want to make sure _storedUncompressedData stays null so 361 // that later when we dispose the archive, this entry loads the compressedBytes, and 362 // copies them straight over 363 _storedUncompressedData.Dispose(); 364 _storedUncompressedData = null; 365 _currentlyOpenForWrite = false; 366 _everOpenedForWrite = false; 367 throw; 368 } 369 } 370 } 371 372 // if they start modifying it, we should make sure it will get deflated 373 CompressionMethod = CompressionMethodValues.Deflate; 374 } 375 376 return _storedUncompressedData; 377 } 378 } 379 380 private CompressionMethodValues CompressionMethod 381 { 382 get { return _storedCompressionMethod; } 383 set 384 { 385 if (value == CompressionMethodValues.Deflate) 386 VersionToExtractAtLeast(ZipVersionNeededValues.Deflate); 387 else if (value == CompressionMethodValues.Deflate64) 388 VersionToExtractAtLeast(ZipVersionNeededValues.Deflate64); 389 _storedCompressionMethod = value; 390 } 391 } 392 DecodeEntryName(byte[] entryNameBytes)393 private string DecodeEntryName(byte[] entryNameBytes) 394 { 395 Debug.Assert(entryNameBytes != null); 396 397 Encoding readEntryNameEncoding; 398 if ((_generalPurposeBitFlag & BitFlagValues.UnicodeFileName) == 0) 399 { 400 readEntryNameEncoding = _archive == null ? 401 Encoding.UTF8 : 402 _archive.EntryNameEncoding ?? Encoding.UTF8; 403 } 404 else 405 { 406 readEntryNameEncoding = Encoding.UTF8; 407 } 408 409 return readEntryNameEncoding.GetString(entryNameBytes); 410 } 411 EncodeEntryName(string entryName, out bool isUTF8)412 private byte[] EncodeEntryName(string entryName, out bool isUTF8) 413 { 414 Debug.Assert(entryName != null); 415 416 Encoding writeEntryNameEncoding; 417 if (_archive != null && _archive.EntryNameEncoding != null) 418 writeEntryNameEncoding = _archive.EntryNameEncoding; 419 else 420 writeEntryNameEncoding = ZipHelper.RequiresUnicode(entryName) ? Encoding.UTF8 : Encoding.ASCII; 421 422 isUTF8 = writeEntryNameEncoding.Equals(Encoding.UTF8); 423 return writeEntryNameEncoding.GetBytes(entryName); 424 } 425 426 // does almost everything you need to do to forget about this entry 427 // writes the local header/data, gets rid of all the data, 428 // closes all of the streams except for the very outermost one that 429 // the user holds on to and is responsible for closing 430 // 431 // after calling this, and only after calling this can we be guaranteed 432 // that we are reading to write the central directory 433 // 434 // should only throw an exception in extremely exceptional cases because it is called from dispose WriteAndFinishLocalEntry()435 internal void WriteAndFinishLocalEntry() 436 { 437 CloseStreams(); 438 WriteLocalFileHeaderAndDataIfNeeded(); 439 UnloadStreams(); 440 } 441 442 // should only throw an exception in extremely exceptional cases because it is called from dispose WriteCentralDirectoryFileHeader()443 internal void WriteCentralDirectoryFileHeader() 444 { 445 // This part is simple, because we should definitely know the sizes by this time 446 BinaryWriter writer = new BinaryWriter(_archive.ArchiveStream); 447 448 // _entryname only gets set when we read in or call moveTo. MoveTo does a check, and 449 // reading in should not be able to produce an entryname longer than ushort.MaxValue 450 Debug.Assert(_storedEntryNameBytes.Length <= ushort.MaxValue); 451 452 // decide if we need the Zip64 extra field: 453 Zip64ExtraField zip64ExtraField = new Zip64ExtraField(); 454 uint compressedSizeTruncated, uncompressedSizeTruncated, offsetOfLocalHeaderTruncated; 455 456 bool zip64Needed = false; 457 458 if (SizesTooLarge() 459 #if DEBUG_FORCE_ZIP64 460 || _archive._forceZip64 461 #endif 462 ) 463 { 464 zip64Needed = true; 465 compressedSizeTruncated = ZipHelper.Mask32Bit; 466 uncompressedSizeTruncated = ZipHelper.Mask32Bit; 467 468 // If we have one of the sizes, the other must go in there as speced for LH, but not necessarily for CH, but we do it anyways 469 zip64ExtraField.CompressedSize = _compressedSize; 470 zip64ExtraField.UncompressedSize = _uncompressedSize; 471 } 472 else 473 { 474 compressedSizeTruncated = (uint)_compressedSize; 475 uncompressedSizeTruncated = (uint)_uncompressedSize; 476 } 477 478 479 if (_offsetOfLocalHeader > uint.MaxValue 480 #if DEBUG_FORCE_ZIP64 481 || _archive._forceZip64 482 #endif 483 ) 484 { 485 zip64Needed = true; 486 offsetOfLocalHeaderTruncated = ZipHelper.Mask32Bit; 487 488 // If we have one of the sizes, the other must go in there as speced for LH, but not necessarily for CH, but we do it anyways 489 zip64ExtraField.LocalHeaderOffset = _offsetOfLocalHeader; 490 } 491 else 492 { 493 offsetOfLocalHeaderTruncated = (uint)_offsetOfLocalHeader; 494 } 495 496 if (zip64Needed) 497 VersionToExtractAtLeast(ZipVersionNeededValues.Zip64); 498 499 // determine if we can fit zip64 extra field and original extra fields all in 500 int bigExtraFieldLength = (zip64Needed ? zip64ExtraField.TotalSize : 0) 501 + (_cdUnknownExtraFields != null ? ZipGenericExtraField.TotalSize(_cdUnknownExtraFields) : 0); 502 ushort extraFieldLength; 503 if (bigExtraFieldLength > ushort.MaxValue) 504 { 505 extraFieldLength = (ushort)(zip64Needed ? zip64ExtraField.TotalSize : 0); 506 _cdUnknownExtraFields = null; 507 } 508 else 509 { 510 extraFieldLength = (ushort)bigExtraFieldLength; 511 } 512 513 writer.Write(ZipCentralDirectoryFileHeader.SignatureConstant); // Central directory file header signature (4 bytes) 514 writer.Write((byte)_versionMadeBySpecification); // Version made by Specification (version) (1 byte) 515 writer.Write((byte)CurrentZipPlatform); // Version made by Compatibility (type) (1 byte) 516 writer.Write((ushort)_versionToExtract); // Minimum version needed to extract (2 bytes) 517 writer.Write((ushort)_generalPurposeBitFlag); // General Purpose bit flag (2 bytes) 518 writer.Write((ushort)CompressionMethod); // The Compression method (2 bytes) 519 writer.Write(ZipHelper.DateTimeToDosTime(_lastModified.DateTime)); // File last modification time and date (4 bytes) 520 writer.Write(_crc32); // CRC-32 (4 bytes) 521 writer.Write(compressedSizeTruncated); // Compressed Size (4 bytes) 522 writer.Write(uncompressedSizeTruncated); // Uncompressed Size (4 bytes) 523 writer.Write((ushort)_storedEntryNameBytes.Length); // File Name Length (2 bytes) 524 writer.Write(extraFieldLength); // Extra Field Length (2 bytes) 525 526 // This should hold because of how we read it originally in ZipCentralDirectoryFileHeader: 527 Debug.Assert((_fileComment == null) || (_fileComment.Length <= ushort.MaxValue)); 528 529 writer.Write(_fileComment != null ? (ushort)_fileComment.Length : (ushort)0); // file comment length 530 writer.Write((ushort)0); // disk number start 531 writer.Write((ushort)0); // internal file attributes 532 writer.Write(_externalFileAttr); // external file attributes 533 writer.Write(offsetOfLocalHeaderTruncated); // offset of local header 534 535 writer.Write(_storedEntryNameBytes); 536 537 // write extra fields 538 if (zip64Needed) 539 zip64ExtraField.WriteBlock(_archive.ArchiveStream); 540 if (_cdUnknownExtraFields != null) 541 ZipGenericExtraField.WriteAllBlocks(_cdUnknownExtraFields, _archive.ArchiveStream); 542 543 if (_fileComment != null) 544 writer.Write(_fileComment); 545 } 546 547 // returns false if fails, will get called on every entry before closing in update mode 548 // can throw InvalidDataException LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded()549 internal bool LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded() 550 { 551 string message; 552 // we should have made this exact call in _archive.Init through ThrowIfOpenable 553 Debug.Assert(IsOpenable(false, true, out message)); 554 555 // load local header's extra fields. it will be null if we couldn't read for some reason 556 if (_originallyInArchive) 557 { 558 _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); 559 560 _lhUnknownExtraFields = ZipLocalFileHeader.GetExtraFields(_archive.ArchiveReader); 561 } 562 563 if (!_everOpenedForWrite && _originallyInArchive) 564 { 565 // we know that it is openable at this point 566 567 _compressedBytes = new byte[(_compressedSize / MaxSingleBufferSize) + 1][]; 568 for (int i = 0; i < _compressedBytes.Length - 1; i++) 569 { 570 _compressedBytes[i] = new byte[MaxSingleBufferSize]; 571 } 572 _compressedBytes[_compressedBytes.Length - 1] = new byte[_compressedSize % MaxSingleBufferSize]; 573 574 _archive.ArchiveStream.Seek(OffsetOfCompressedData, SeekOrigin.Begin); 575 576 for (int i = 0; i < _compressedBytes.Length - 1; i++) 577 { 578 ZipHelper.ReadBytes(_archive.ArchiveStream, _compressedBytes[i], MaxSingleBufferSize); 579 } 580 ZipHelper.ReadBytes(_archive.ArchiveStream, _compressedBytes[_compressedBytes.Length - 1], (int)(_compressedSize % MaxSingleBufferSize)); 581 } 582 583 return true; 584 } 585 ThrowIfNotOpenable(bool needToUncompress, bool needToLoadIntoMemory)586 internal void ThrowIfNotOpenable(bool needToUncompress, bool needToLoadIntoMemory) 587 { 588 string message; 589 if (!IsOpenable(needToUncompress, needToLoadIntoMemory, out message)) 590 throw new InvalidDataException(message); 591 } 592 GetDataCompressor(Stream backingStream, bool leaveBackingStreamOpen, EventHandler onClose)593 private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, bool leaveBackingStreamOpen, EventHandler onClose) 594 { 595 // stream stack: backingStream -> DeflateStream -> CheckSumWriteStream 596 597 // we should always be compressing with deflate. Stored is used for empty files, but we don't actually 598 // call through this function for that - we just write the stored value in the header 599 Debug.Assert(CompressionMethod == CompressionMethodValues.Deflate); 600 601 Stream compressorStream = _compressionLevel.HasValue ? 602 new DeflateStream(backingStream, _compressionLevel.Value, leaveBackingStreamOpen) : 603 new DeflateStream(backingStream, CompressionMode.Compress, leaveBackingStreamOpen); 604 605 bool isIntermediateStream = true; 606 607 bool leaveCompressorStreamOpenOnClose = leaveBackingStreamOpen && !isIntermediateStream; 608 var checkSumStream = new CheckSumAndSizeWriteStream( 609 compressorStream, 610 backingStream, 611 leaveCompressorStreamOpenOnClose, 612 this, 613 onClose, 614 (long initialPosition, long currentPosition, uint checkSum, Stream backing, ZipArchiveEntry thisRef, EventHandler closeHandler) => 615 { 616 thisRef._crc32 = checkSum; 617 thisRef._uncompressedSize = currentPosition; 618 thisRef._compressedSize = backing.Position - initialPosition; 619 closeHandler?.Invoke(thisRef, EventArgs.Empty); 620 }); 621 622 return checkSumStream; 623 } 624 GetDataDecompressor(Stream compressedStreamToRead)625 private Stream GetDataDecompressor(Stream compressedStreamToRead) 626 { 627 Stream uncompressedStream = null; 628 switch (CompressionMethod) 629 { 630 case CompressionMethodValues.Deflate: 631 uncompressedStream = new DeflateStream(compressedStreamToRead, CompressionMode.Decompress); 632 break; 633 case CompressionMethodValues.Deflate64: 634 uncompressedStream = new DeflateManagedStream(compressedStreamToRead, CompressionMethodValues.Deflate64); 635 break; 636 case CompressionMethodValues.Stored: 637 default: 638 // we can assume that only deflate/deflate64/stored are allowed because we assume that 639 // IsOpenable is checked before this function is called 640 Debug.Assert(CompressionMethod == CompressionMethodValues.Stored); 641 642 uncompressedStream = compressedStreamToRead; 643 break; 644 } 645 646 return uncompressedStream; 647 } 648 OpenInReadMode(bool checkOpenable)649 private Stream OpenInReadMode(bool checkOpenable) 650 { 651 if (checkOpenable) 652 ThrowIfNotOpenable(needToUncompress: true, needToLoadIntoMemory: false); 653 654 Stream compressedStream = new SubReadStream(_archive.ArchiveStream, OffsetOfCompressedData, _compressedSize); 655 return GetDataDecompressor(compressedStream); 656 } 657 OpenInWriteMode()658 private Stream OpenInWriteMode() 659 { 660 if (_everOpenedForWrite) 661 throw new IOException(SR.CreateModeWriteOnceAndOneEntryAtATime); 662 663 // we assume that if another entry grabbed the archive stream, that it set this entry's _everOpenedForWrite property to true by calling WriteLocalFileHeaderIfNeeed 664 _archive.DebugAssertIsStillArchiveStreamOwner(this); 665 666 _everOpenedForWrite = true; 667 CheckSumAndSizeWriteStream crcSizeStream = GetDataCompressor(_archive.ArchiveStream, true, (object o, EventArgs e) => 668 { 669 // release the archive stream 670 var entry = (ZipArchiveEntry)o; 671 entry._archive.ReleaseArchiveStream(entry); 672 entry._outstandingWriteStream = null; 673 }); 674 _outstandingWriteStream = new DirectToArchiveWriterStream(crcSizeStream, this); 675 676 return new WrappedStream(baseStream: _outstandingWriteStream, closeBaseStream: true); 677 } 678 OpenInUpdateMode()679 private Stream OpenInUpdateMode() 680 { 681 if (_currentlyOpenForWrite) 682 throw new IOException(SR.UpdateModeOneStream); 683 684 ThrowIfNotOpenable(needToUncompress: true, needToLoadIntoMemory: true); 685 686 _everOpenedForWrite = true; 687 _currentlyOpenForWrite = true; 688 // always put it at the beginning for them 689 UncompressedData.Seek(0, SeekOrigin.Begin); 690 return new WrappedStream(UncompressedData, this, thisRef => 691 { 692 // once they close, we know uncompressed length, but still not compressed length 693 // so we don't fill in any size information 694 // those fields get figured out when we call GetCompressor as we write it to 695 // the actual archive 696 thisRef._currentlyOpenForWrite = false; 697 }); 698 } 699 IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out string message)700 private bool IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out string message) 701 { 702 message = null; 703 704 if (_originallyInArchive) 705 { 706 if (needToUncompress) 707 { 708 if (CompressionMethod != CompressionMethodValues.Stored && 709 CompressionMethod != CompressionMethodValues.Deflate && 710 CompressionMethod != CompressionMethodValues.Deflate64) 711 { 712 switch (CompressionMethod) 713 { 714 case CompressionMethodValues.BZip2: 715 case CompressionMethodValues.LZMA: 716 message = SR.Format(SR.UnsupportedCompressionMethod, CompressionMethod.ToString()); 717 break; 718 default: 719 message = SR.UnsupportedCompression; 720 break; 721 } 722 return false; 723 } 724 } 725 if (_diskNumberStart != _archive.NumberOfThisDisk) 726 { 727 message = SR.SplitSpanned; 728 return false; 729 } 730 if (_offsetOfLocalHeader > _archive.ArchiveStream.Length) 731 { 732 message = SR.LocalFileHeaderCorrupt; 733 return false; 734 } 735 _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); 736 if (!ZipLocalFileHeader.TrySkipBlock(_archive.ArchiveReader)) 737 { 738 message = SR.LocalFileHeaderCorrupt; 739 return false; 740 } 741 // when this property gets called, some duplicated work 742 if (OffsetOfCompressedData + _compressedSize > _archive.ArchiveStream.Length) 743 { 744 message = SR.LocalFileHeaderCorrupt; 745 return false; 746 } 747 // This limitation originally existed because a) it is unreasonable to load > 4GB into memory 748 // but also because the stream reading functions make it hard. This has been updated to handle 749 // this scenario in a 64-bit process using multiple buffers, delivered first as an OOB for 750 // compatibility. 751 if (needToLoadIntoMemory) 752 { 753 if (_compressedSize > int.MaxValue) 754 { 755 if (!s_allowLargeZipArchiveEntriesInUpdateMode) 756 { 757 message = SR.EntryTooLarge; 758 return false; 759 } 760 } 761 } 762 } 763 764 return true; 765 } 766 SizesTooLarge()767 private bool SizesTooLarge() => _compressedSize > uint.MaxValue || _uncompressedSize > uint.MaxValue; 768 769 // return value is true if we allocated an extra field for 64 bit headers, un/compressed size WriteLocalFileHeader(bool isEmptyFile)770 private bool WriteLocalFileHeader(bool isEmptyFile) 771 { 772 BinaryWriter writer = new BinaryWriter(_archive.ArchiveStream); 773 774 // _entryname only gets set when we read in or call moveTo. MoveTo does a check, and 775 // reading in should not be able to produce an entryname longer than ushort.MaxValue 776 Debug.Assert(_storedEntryNameBytes.Length <= ushort.MaxValue); 777 778 // decide if we need the Zip64 extra field: 779 Zip64ExtraField zip64ExtraField = new Zip64ExtraField(); 780 bool zip64Used = false; 781 uint compressedSizeTruncated, uncompressedSizeTruncated; 782 783 // if we already know that we have an empty file don't worry about anything, just do a straight shot of the header 784 if (isEmptyFile) 785 { 786 CompressionMethod = CompressionMethodValues.Stored; 787 compressedSizeTruncated = 0; 788 uncompressedSizeTruncated = 0; 789 Debug.Assert(_compressedSize == 0); 790 Debug.Assert(_uncompressedSize == 0); 791 Debug.Assert(_crc32 == 0); 792 } 793 else 794 { 795 // if we have a non-seekable stream, don't worry about sizes at all, and just set the right bit 796 // if we are using the data descriptor, then sizes and crc should be set to 0 in the header 797 if (_archive.Mode == ZipArchiveMode.Create && _archive.ArchiveStream.CanSeek == false && !isEmptyFile) 798 { 799 _generalPurposeBitFlag |= BitFlagValues.DataDescriptor; 800 zip64Used = false; 801 compressedSizeTruncated = 0; 802 uncompressedSizeTruncated = 0; 803 // the crc should not have been set if we are in create mode, but clear it just to be sure 804 Debug.Assert(_crc32 == 0); 805 } 806 else // if we are not in streaming mode, we have to decide if we want to write zip64 headers 807 { 808 if (SizesTooLarge() 809 #if DEBUG_FORCE_ZIP64 810 || (_archive._forceZip64 && _archive.Mode == ZipArchiveMode.Update) 811 #endif 812 ) 813 { 814 zip64Used = true; 815 compressedSizeTruncated = ZipHelper.Mask32Bit; 816 uncompressedSizeTruncated = ZipHelper.Mask32Bit; 817 818 // prepare Zip64 extra field object. If we have one of the sizes, the other must go in there 819 zip64ExtraField.CompressedSize = _compressedSize; 820 zip64ExtraField.UncompressedSize = _uncompressedSize; 821 822 VersionToExtractAtLeast(ZipVersionNeededValues.Zip64); 823 } 824 else 825 { 826 zip64Used = false; 827 compressedSizeTruncated = (uint)_compressedSize; 828 uncompressedSizeTruncated = (uint)_uncompressedSize; 829 } 830 } 831 } 832 833 // save offset 834 _offsetOfLocalHeader = writer.BaseStream.Position; 835 836 // calculate extra field. if zip64 stuff + original extraField aren't going to fit, dump the original extraField, because this is more important 837 int bigExtraFieldLength = (zip64Used ? zip64ExtraField.TotalSize : 0) 838 + (_lhUnknownExtraFields != null ? ZipGenericExtraField.TotalSize(_lhUnknownExtraFields) : 0); 839 ushort extraFieldLength; 840 if (bigExtraFieldLength > ushort.MaxValue) 841 { 842 extraFieldLength = (ushort)(zip64Used ? zip64ExtraField.TotalSize : 0); 843 _lhUnknownExtraFields = null; 844 } 845 else 846 { 847 extraFieldLength = (ushort)bigExtraFieldLength; 848 } 849 850 // write header 851 writer.Write(ZipLocalFileHeader.SignatureConstant); 852 writer.Write((ushort)_versionToExtract); 853 writer.Write((ushort)_generalPurposeBitFlag); 854 writer.Write((ushort)CompressionMethod); 855 writer.Write(ZipHelper.DateTimeToDosTime(_lastModified.DateTime)); // uint 856 writer.Write(_crc32); // uint 857 writer.Write(compressedSizeTruncated); // uint 858 writer.Write(uncompressedSizeTruncated); // uint 859 writer.Write((ushort)_storedEntryNameBytes.Length); 860 writer.Write(extraFieldLength); // ushort 861 862 writer.Write(_storedEntryNameBytes); 863 864 if (zip64Used) 865 zip64ExtraField.WriteBlock(_archive.ArchiveStream); 866 if (_lhUnknownExtraFields != null) 867 ZipGenericExtraField.WriteAllBlocks(_lhUnknownExtraFields, _archive.ArchiveStream); 868 869 return zip64Used; 870 } 871 WriteLocalFileHeaderAndDataIfNeeded()872 private void WriteLocalFileHeaderAndDataIfNeeded() 873 { 874 // _storedUncompressedData gets frozen here, and is what gets written to the file 875 if (_storedUncompressedData != null || _compressedBytes != null) 876 { 877 if (_storedUncompressedData != null) 878 { 879 _uncompressedSize = _storedUncompressedData.Length; 880 881 //The compressor fills in CRC and sizes 882 //The DirectToArchiveWriterStream writes headers and such 883 using (Stream entryWriter = new DirectToArchiveWriterStream( 884 GetDataCompressor(_archive.ArchiveStream, true, null), 885 this)) 886 { 887 _storedUncompressedData.Seek(0, SeekOrigin.Begin); 888 _storedUncompressedData.CopyTo(entryWriter); 889 _storedUncompressedData.Dispose(); 890 _storedUncompressedData = null; 891 } 892 } 893 else 894 { 895 // we know the sizes at this point, so just go ahead and write the headers 896 if (_uncompressedSize == 0) 897 CompressionMethod = CompressionMethodValues.Stored; 898 WriteLocalFileHeader(isEmptyFile: false); 899 foreach (byte[] compressedBytes in _compressedBytes) 900 { 901 _archive.ArchiveStream.Write(compressedBytes, 0, compressedBytes.Length); 902 } 903 } 904 } 905 else // there is no data in the file, but if we are in update mode, we still need to write a header 906 { 907 if (_archive.Mode == ZipArchiveMode.Update || !_everOpenedForWrite) 908 { 909 _everOpenedForWrite = true; 910 WriteLocalFileHeader(isEmptyFile: true); 911 } 912 } 913 } 914 915 // Using _offsetOfLocalHeader, seeks back to where CRC and sizes should be in the header, 916 // writes them, then seeks back to where you started 917 // Assumes that the stream is currently at the end of the data WriteCrcAndSizesInLocalHeader(bool zip64HeaderUsed)918 private void WriteCrcAndSizesInLocalHeader(bool zip64HeaderUsed) 919 { 920 long finalPosition = _archive.ArchiveStream.Position; 921 BinaryWriter writer = new BinaryWriter(_archive.ArchiveStream); 922 923 bool zip64Needed = SizesTooLarge() 924 #if DEBUG_FORCE_ZIP64 925 || _archive._forceZip64 926 #endif 927 ; 928 929 bool pretendStreaming = zip64Needed && !zip64HeaderUsed; 930 931 uint compressedSizeTruncated = zip64Needed ? ZipHelper.Mask32Bit : (uint)_compressedSize; 932 uint uncompressedSizeTruncated = zip64Needed ? ZipHelper.Mask32Bit : (uint)_uncompressedSize; 933 934 // first step is, if we need zip64, but didn't allocate it, pretend we did a stream write, because 935 // we can't go back and give ourselves the space that the extra field needs. 936 // we do this by setting the correct property in the bit flag 937 if (pretendStreaming) 938 { 939 _generalPurposeBitFlag |= BitFlagValues.DataDescriptor; 940 941 _archive.ArchiveStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.OffsetToBitFlagFromHeaderStart, 942 SeekOrigin.Begin); 943 writer.Write((ushort)_generalPurposeBitFlag); 944 } 945 946 // next step is fill out the 32-bit size values in the normal header. we can't assume that 947 // they are correct. we also write the CRC 948 _archive.ArchiveStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.OffsetToCrcFromHeaderStart, 949 SeekOrigin.Begin); 950 if (!pretendStreaming) 951 { 952 writer.Write(_crc32); 953 writer.Write(compressedSizeTruncated); 954 writer.Write(uncompressedSizeTruncated); 955 } 956 else // but if we are pretending to stream, we want to fill in with zeroes 957 { 958 writer.Write((uint)0); 959 writer.Write((uint)0); 960 writer.Write((uint)0); 961 } 962 963 // next step: if we wrote the 64 bit header initially, a different implementation might 964 // try to read it, even if the 32-bit size values aren't masked. thus, we should always put the 965 // correct size information in there. note that order of uncomp/comp is switched, and these are 966 // 64-bit values 967 // also, note that in order for this to be correct, we have to insure that the zip64 extra field 968 // is always the first extra field that is written 969 if (zip64HeaderUsed) 970 { 971 _archive.ArchiveStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.SizeOfLocalHeader 972 + _storedEntryNameBytes.Length + Zip64ExtraField.OffsetToFirstField, 973 SeekOrigin.Begin); 974 writer.Write(_uncompressedSize); 975 writer.Write(_compressedSize); 976 977 _archive.ArchiveStream.Seek(finalPosition, SeekOrigin.Begin); 978 } 979 980 // now go to the where we were. assume that this is the end of the data 981 _archive.ArchiveStream.Seek(finalPosition, SeekOrigin.Begin); 982 983 // if we are pretending we did a stream write, we want to write the data descriptor out 984 // the data descriptor can have 32-bit sizes or 64-bit sizes. In this case, we always use 985 // 64-bit sizes 986 if (pretendStreaming) 987 { 988 writer.Write(_crc32); 989 writer.Write(_compressedSize); 990 writer.Write(_uncompressedSize); 991 } 992 } 993 WriteDataDescriptor()994 private void WriteDataDescriptor() 995 { 996 // data descriptor can be 32-bit or 64-bit sizes. 32-bit is more compatible, so use that if possible 997 // signature is optional but recommended by the spec 998 999 BinaryWriter writer = new BinaryWriter(_archive.ArchiveStream); 1000 1001 writer.Write(ZipLocalFileHeader.DataDescriptorSignature); 1002 writer.Write(_crc32); 1003 if (SizesTooLarge()) 1004 { 1005 writer.Write(_compressedSize); 1006 writer.Write(_uncompressedSize); 1007 } 1008 else 1009 { 1010 writer.Write((uint)_compressedSize); 1011 writer.Write((uint)_uncompressedSize); 1012 } 1013 } 1014 UnloadStreams()1015 private void UnloadStreams() 1016 { 1017 if (_storedUncompressedData != null) 1018 _storedUncompressedData.Dispose(); 1019 _compressedBytes = null; 1020 _outstandingWriteStream = null; 1021 } 1022 CloseStreams()1023 private void CloseStreams() 1024 { 1025 // if the user left the stream open, close the underlying stream for them 1026 if (_outstandingWriteStream != null) 1027 { 1028 _outstandingWriteStream.Dispose(); 1029 } 1030 } 1031 VersionToExtractAtLeast(ZipVersionNeededValues value)1032 private void VersionToExtractAtLeast(ZipVersionNeededValues value) 1033 { 1034 if (_versionToExtract < value) 1035 { 1036 _versionToExtract = value; 1037 } 1038 if (_versionMadeBySpecification < value) 1039 { 1040 _versionMadeBySpecification = value; 1041 } 1042 } 1043 ThrowIfInvalidArchive()1044 private void ThrowIfInvalidArchive() 1045 { 1046 if (_archive == null) 1047 throw new InvalidOperationException(SR.DeletedEntry); 1048 _archive.ThrowIfDisposed(); 1049 } 1050 1051 /// <summary> 1052 /// Gets the file name of the path based on Windows path separator characters 1053 /// </summary> GetFileName_Windows(string path)1054 private static string GetFileName_Windows(string path) 1055 { 1056 int length = path.Length; 1057 for (int i = length; --i >= 0;) 1058 { 1059 char ch = path[i]; 1060 if (ch == '\\' || ch == '/' || ch == ':') 1061 return path.Substring(i + 1); 1062 } 1063 return path; 1064 } 1065 1066 /// <summary> 1067 /// Gets the file name of the path based on Unix path separator characters 1068 /// </summary> GetFileName_Unix(string path)1069 private static string GetFileName_Unix(string path) 1070 { 1071 int length = path.Length; 1072 for (int i = length; --i >= 0;) 1073 if (path[i] == '/') 1074 return path.Substring(i + 1); 1075 return path; 1076 } 1077 1078 private sealed partial class DirectToArchiveWriterStream : Stream 1079 { 1080 private long _position; 1081 private CheckSumAndSizeWriteStream _crcSizeStream; 1082 private bool _everWritten; 1083 private bool _isDisposed; 1084 private ZipArchiveEntry _entry; 1085 private bool _usedZip64inLH; 1086 private bool _canWrite; 1087 1088 // makes the assumption that somewhere down the line, crcSizeStream is eventually writing directly to the archive 1089 // this class calls other functions on ZipArchiveEntry that write directly to the archive DirectToArchiveWriterStream(CheckSumAndSizeWriteStream crcSizeStream, ZipArchiveEntry entry)1090 public DirectToArchiveWriterStream(CheckSumAndSizeWriteStream crcSizeStream, ZipArchiveEntry entry) 1091 { 1092 _position = 0; 1093 _crcSizeStream = crcSizeStream; 1094 _everWritten = false; 1095 _isDisposed = false; 1096 _entry = entry; 1097 _usedZip64inLH = false; 1098 _canWrite = true; 1099 } 1100 1101 public override long Length 1102 { 1103 get 1104 { 1105 ThrowIfDisposed(); 1106 throw new NotSupportedException(SR.SeekingNotSupported); 1107 } 1108 } 1109 public override long Position 1110 { 1111 get 1112 { 1113 ThrowIfDisposed(); 1114 return _position; 1115 } 1116 set 1117 { 1118 ThrowIfDisposed(); 1119 throw new NotSupportedException(SR.SeekingNotSupported); 1120 } 1121 } 1122 1123 public override bool CanRead => false; 1124 public override bool CanSeek => false; 1125 public override bool CanWrite => _canWrite; 1126 ThrowIfDisposed()1127 private void ThrowIfDisposed() 1128 { 1129 if (_isDisposed) 1130 throw new ObjectDisposedException(GetType().ToString(), SR.HiddenStreamName); 1131 } 1132 Read(byte[] buffer, int offset, int count)1133 public override int Read(byte[] buffer, int offset, int count) 1134 { 1135 ThrowIfDisposed(); 1136 throw new NotSupportedException(SR.ReadingNotSupported); 1137 } 1138 Seek(long offset, SeekOrigin origin)1139 public override long Seek(long offset, SeekOrigin origin) 1140 { 1141 ThrowIfDisposed(); 1142 throw new NotSupportedException(SR.SeekingNotSupported); 1143 } 1144 SetLength(long value)1145 public override void SetLength(long value) 1146 { 1147 ThrowIfDisposed(); 1148 throw new NotSupportedException(SR.SetLengthRequiresSeekingAndWriting); 1149 } 1150 1151 // careful: assumes that write is the only way to write to the stream, if writebyte/beginwrite are implemented 1152 // they must set _everWritten, etc. Write(byte[] buffer, int offset, int count)1153 public override void Write(byte[] buffer, int offset, int count) 1154 { 1155 //we can't pass the argument checking down a level 1156 if (buffer == null) 1157 throw new ArgumentNullException(nameof(buffer)); 1158 if (offset < 0) 1159 throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentNeedNonNegative); 1160 if (count < 0) 1161 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentNeedNonNegative); 1162 if ((buffer.Length - offset) < count) 1163 throw new ArgumentException(SR.OffsetLengthInvalid); 1164 1165 ThrowIfDisposed(); 1166 Debug.Assert(CanWrite); 1167 1168 // if we're not actually writing anything, we don't want to trigger the header 1169 if (count == 0) 1170 return; 1171 1172 if (!_everWritten) 1173 { 1174 _everWritten = true; 1175 // write local header, we are good to go 1176 _usedZip64inLH = _entry.WriteLocalFileHeader(isEmptyFile: false); 1177 } 1178 1179 _crcSizeStream.Write(buffer, offset, count); 1180 _position += count; 1181 } 1182 Flush()1183 public override void Flush() 1184 { 1185 ThrowIfDisposed(); 1186 Debug.Assert(CanWrite); 1187 1188 _crcSizeStream.Flush(); 1189 } 1190 Dispose(bool disposing)1191 protected override void Dispose(bool disposing) 1192 { 1193 if (disposing && !_isDisposed) 1194 { 1195 _crcSizeStream.Dispose(); // now we have size/crc info 1196 1197 if (!_everWritten) 1198 { 1199 // write local header, no data, so we use stored 1200 _entry.WriteLocalFileHeader(isEmptyFile: true); 1201 } 1202 else 1203 { 1204 // go back and finish writing 1205 if (_entry._archive.ArchiveStream.CanSeek) 1206 // finish writing local header if we have seek capabilities 1207 1208 _entry.WriteCrcAndSizesInLocalHeader(_usedZip64inLH); 1209 else 1210 // write out data descriptor if we don't have seek capabilities 1211 _entry.WriteDataDescriptor(); 1212 } 1213 _canWrite = false; 1214 _isDisposed = true; 1215 } 1216 1217 base.Dispose(disposing); 1218 } 1219 } 1220 1221 [Flags] 1222 private enum BitFlagValues : ushort { DataDescriptor = 0x8, UnicodeFileName = 0x800 } 1223 1224 internal enum CompressionMethodValues : ushort { Stored = 0x0, Deflate = 0x8, Deflate64 = 0x9, BZip2 = 0xC, LZMA = 0xE } 1225 } 1226 } 1227