| | | 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 | | using System.Buffers.Binary; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.Diagnostics; |
| | | 7 | | using System.Diagnostics.CodeAnalysis; |
| | | 8 | | using System.Text; |
| | | 9 | | using System.Threading; |
| | | 10 | | using System.Threading.Tasks; |
| | | 11 | | |
| | | 12 | | using static System.IO.Compression.ZipArchiveEntryConstants; |
| | | 13 | | |
| | | 14 | | namespace System.IO.Compression |
| | | 15 | | { |
| | | 16 | | // The disposable fields that this class owns get disposed when the ZipArchive it belongs to gets disposed |
| | | 17 | | public partial class ZipArchiveEntry |
| | | 18 | | { |
| | | 19 | | private ZipArchive _archive; |
| | | 20 | | private readonly bool _originallyInArchive; |
| | | 21 | | private readonly uint _diskNumberStart; |
| | | 22 | | private readonly ZipVersionMadeByPlatform _versionMadeByPlatform; |
| | | 23 | | private ZipVersionNeededValues _versionMadeBySpecification; |
| | | 24 | | private ZipVersionNeededValues _versionToExtract; |
| | | 25 | | private BitFlagValues _generalPurposeBitFlag; |
| | | 26 | | private ZipCompressionMethod _storedCompressionMethod; |
| | | 27 | | private DateTimeOffset _lastModified; |
| | | 28 | | private long _compressedSize; |
| | | 29 | | private long _uncompressedSize; |
| | | 30 | | private long _offsetOfLocalHeader; |
| | | 31 | | private long? _storedOffsetOfCompressedData; |
| | | 32 | | private long _endOfLocalEntryData; |
| | | 33 | | private uint _crc32; |
| | | 34 | | // An array of buffers, each a maximum of MaxSingleBufferSize in size |
| | | 35 | | private byte[][]? _compressedBytes; |
| | | 36 | | private MemoryStream? _storedUncompressedData; |
| | | 37 | | private bool _currentlyOpenForWrite; |
| | | 38 | | private bool _everOpenedForWrite; |
| | | 39 | | private Stream? _outstandingWriteStream; |
| | | 40 | | private uint _externalFileAttr; |
| | | 41 | | private string _storedEntryName; |
| | | 42 | | private byte[] _storedEntryNameBytes; |
| | | 43 | | // only apply to update mode |
| | | 44 | | private List<ZipGenericExtraField>? _cdUnknownExtraFields; |
| | | 45 | | private byte[]? _cdTrailingExtraFieldData; |
| | | 46 | | private List<ZipGenericExtraField>? _lhUnknownExtraFields; |
| | | 47 | | private byte[]? _lhTrailingExtraFieldData; |
| | | 48 | | private byte[] _fileComment; |
| | | 49 | | private ZipEncryptionMethod _encryptionMethod; |
| | | 50 | | private readonly CompressionLevel _compressionLevel; |
| | | 51 | | private ZipCompressionMethod _headerCompressionMethod; |
| | | 52 | | private ushort? _aeVersion; |
| | | 53 | | // Cached derived key material for encrypted entries to allow updating in place. |
| | | 54 | | // Only one of these is set at a time, depending on the encryption method. |
| | | 55 | | private ZipCryptoKeys? _derivedZipCryptoKeyMaterial; |
| | | 56 | | private WinZipAesKeyMaterial? _derivedAesKeyMaterial; |
| | | 57 | | // Pre-read AES salt from the local file data area during central directory parsing. |
| | | 58 | | // This allows async open methods to derive keys without synchronous I/O. |
| | | 59 | | private byte[]? _aesSalt; |
| | | 60 | | internal const ushort WinZipAesMethod = 99; |
| | | 61 | | // Initializes a ZipArchiveEntry instance for an existing archive entry. |
| | 29756 | 62 | | internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd) |
| | 29756 | 63 | | { |
| | 29756 | 64 | | _archive = archive; |
| | | 65 | | |
| | 29756 | 66 | | _originallyInArchive = true; |
| | | 67 | | // It's possible for the CompressionMethod setter and DetectEntryNameVersion to update this, even without an |
| | | 68 | | // changes. This can occur if a ZipArchive instance runs in Update mode and opens a stream with invalid data |
| | | 69 | | // a situation, both the local file header and the central directory header will be rewritten (to prevent th |
| | | 70 | | // from falling out of sync when the central directory header is rewritten.) |
| | 29756 | 71 | | Changes = ZipArchive.ChangeState.Unchanged; |
| | | 72 | | |
| | 29756 | 73 | | _diskNumberStart = cd.DiskNumberStart; |
| | 29756 | 74 | | _versionMadeByPlatform = (ZipVersionMadeByPlatform)cd.VersionMadeByCompatibility; |
| | 29756 | 75 | | _versionMadeBySpecification = (ZipVersionNeededValues)cd.VersionMadeBySpecification; |
| | 29756 | 76 | | _versionToExtract = (ZipVersionNeededValues)cd.VersionNeededToExtract; |
| | 29756 | 77 | | _generalPurposeBitFlag = (BitFlagValues)cd.GeneralPurposeBitFlag; |
| | | 78 | | // Initialize _headerCompressionMethod from the central directory. |
| | | 79 | | // For AES entries, this will be 99 (WinZip AES wrapper indicator) and never changes. |
| | 29756 | 80 | | _headerCompressionMethod = (ZipCompressionMethod)cd.CompressionMethod; |
| | | 81 | | // For AES-encrypted entries, the real compression method is stored in the AES extra field (0x9901) |
| | | 82 | | // Parse it now so that people can see the actual value before opening the entry. |
| | 29756 | 83 | | if (IsEncrypted && cd.AesExtraField.HasValue) |
| | 0 | 84 | | { |
| | 0 | 85 | | WinZipAesExtraField aesField = cd.AesExtraField.Value; |
| | | 86 | | // Set the real compression method from the AES extra field |
| | 0 | 87 | | CompressionMethod = (ZipCompressionMethod)aesField.CompressionMethod; |
| | | 88 | | |
| | | 89 | | // Also parse remaining needed metadata now |
| | 0 | 90 | | _aeVersion = aesField.VendorVersion; |
| | 0 | 91 | | Encryption = aesField.AesStrength switch |
| | 0 | 92 | | { |
| | 0 | 93 | | 1 => ZipEncryptionMethod.Aes128, |
| | 0 | 94 | | 2 => ZipEncryptionMethod.Aes192, |
| | 0 | 95 | | 3 => ZipEncryptionMethod.Aes256, |
| | 0 | 96 | | _ => throw new InvalidDataException(SR.InvalidAesStrength) |
| | 0 | 97 | | }; |
| | 0 | 98 | | } |
| | 29756 | 99 | | else if (IsEncrypted) |
| | 1908 | 100 | | { |
| | 1908 | 101 | | if ((_generalPurposeBitFlag & BitFlagValues.StrongEncryption) != 0) |
| | 610 | 102 | | { |
| | 610 | 103 | | Encryption = ZipEncryptionMethod.Unknown; |
| | 610 | 104 | | } |
| | | 105 | | else |
| | 1298 | 106 | | { |
| | | 107 | | // Encrypted but no AES extra field means ZipCrypto |
| | 1298 | 108 | | Encryption = ZipEncryptionMethod.ZipCrypto; |
| | 1298 | 109 | | } |
| | 1908 | 110 | | CompressionMethod = (ZipCompressionMethod)cd.CompressionMethod; |
| | 1908 | 111 | | } |
| | | 112 | | else |
| | 27848 | 113 | | { |
| | | 114 | | // Non-AES entry: compression method from CD is the real method |
| | 27848 | 115 | | CompressionMethod = (ZipCompressionMethod)cd.CompressionMethod; |
| | 27848 | 116 | | } |
| | | 117 | | |
| | 29756 | 118 | | _lastModified = new DateTimeOffset(ZipHelper.DosTimeToDateTime(cd.LastModified)); |
| | 29756 | 119 | | _compressedSize = cd.CompressedSize; |
| | 29756 | 120 | | _uncompressedSize = cd.UncompressedSize; |
| | 29756 | 121 | | _externalFileAttr = cd.ExternalFileAttributes; |
| | 29756 | 122 | | _offsetOfLocalHeader = cd.RelativeOffsetOfLocalHeader; |
| | | 123 | | // we don't know this yet: should be _offsetOfLocalHeader + 30 + _storedEntryNameBytes.Length + extrafieldle |
| | | 124 | | // but entryname/extra length could be different in LH |
| | 29756 | 125 | | _storedOffsetOfCompressedData = null; |
| | 29756 | 126 | | _crc32 = cd.Crc32; |
| | | 127 | | |
| | 29756 | 128 | | _compressedBytes = null; |
| | 29756 | 129 | | _storedUncompressedData = null; |
| | 29756 | 130 | | _currentlyOpenForWrite = false; |
| | 29756 | 131 | | _everOpenedForWrite = false; |
| | 29756 | 132 | | _outstandingWriteStream = null; |
| | | 133 | | |
| | 29756 | 134 | | _storedEntryNameBytes = cd.Filename; |
| | 29756 | 135 | | _storedEntryName = DecodeEntryString(_storedEntryNameBytes); |
| | 29756 | 136 | | DetectEntryNameVersion(); |
| | | 137 | | |
| | 29756 | 138 | | _lhUnknownExtraFields = null; |
| | | 139 | | // the cd should have this as null if we aren't in Update mode |
| | 29756 | 140 | | _cdUnknownExtraFields = cd.ExtraFields; |
| | 29756 | 141 | | _cdTrailingExtraFieldData = cd.TrailingExtraFieldData; |
| | | 142 | | |
| | 29756 | 143 | | _fileComment = cd.FileComment; |
| | | 144 | | |
| | 29756 | 145 | | _compressionLevel = MapCompressionLevel(_generalPurposeBitFlag, CompressionMethod); |
| | 29756 | 146 | | } |
| | | 147 | | // Initializes a ZipArchiveEntry instance for a new archive entry with a specified compression level. |
| | | 148 | | internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel compressionLevel) |
| | 0 | 149 | | : this(archive, entryName) |
| | 0 | 150 | | { |
| | 0 | 151 | | _compressionLevel = compressionLevel; |
| | 0 | 152 | | if (_compressionLevel == CompressionLevel.NoCompression) |
| | 0 | 153 | | { |
| | 0 | 154 | | CompressionMethod = ZipCompressionMethod.Stored; |
| | 0 | 155 | | } |
| | 0 | 156 | | _generalPurposeBitFlag = MapDeflateCompressionOption(_generalPurposeBitFlag, _compressionLevel, CompressionM |
| | 0 | 157 | | } |
| | | 158 | | |
| | | 159 | | // Initializes a ZipArchiveEntry instance for a new archive entry. |
| | 0 | 160 | | internal ZipArchiveEntry(ZipArchive archive, string entryName) |
| | 0 | 161 | | { |
| | 0 | 162 | | _archive = archive; |
| | | 163 | | |
| | 0 | 164 | | _originallyInArchive = false; |
| | | 165 | | |
| | 0 | 166 | | _diskNumberStart = 0; |
| | 0 | 167 | | _versionMadeByPlatform = CurrentZipPlatform; |
| | 0 | 168 | | _versionMadeBySpecification = ZipVersionNeededValues.Default; |
| | 0 | 169 | | _versionToExtract = ZipVersionNeededValues.Default; // this must happen before following two assignment |
| | 0 | 170 | | _compressionLevel = CompressionLevel.Optimal; |
| | 0 | 171 | | CompressionMethod = ZipCompressionMethod.Deflate; |
| | 0 | 172 | | _generalPurposeBitFlag = MapDeflateCompressionOption(0, _compressionLevel, CompressionMethod); |
| | 0 | 173 | | _lastModified = DateTimeOffset.Now; |
| | | 174 | | |
| | 0 | 175 | | _compressedSize = 0; // we don't know these yet |
| | 0 | 176 | | _uncompressedSize = 0; |
| | 0 | 177 | | _externalFileAttr = entryName.EndsWith(Path.DirectorySeparatorChar) || entryName.EndsWith(Path.AltDirectoryS |
| | 0 | 178 | | ? DefaultDirectoryExternalAttributes |
| | 0 | 179 | | : DefaultFileExternalAttributes; |
| | | 180 | | |
| | 0 | 181 | | _offsetOfLocalHeader = 0; |
| | 0 | 182 | | _storedOffsetOfCompressedData = null; |
| | 0 | 183 | | _crc32 = 0; |
| | | 184 | | |
| | 0 | 185 | | _compressedBytes = null; |
| | 0 | 186 | | _storedUncompressedData = null; |
| | 0 | 187 | | _currentlyOpenForWrite = false; |
| | 0 | 188 | | _everOpenedForWrite = false; |
| | 0 | 189 | | _outstandingWriteStream = null; |
| | | 190 | | |
| | 0 | 191 | | FullName = entryName; |
| | | 192 | | |
| | 0 | 193 | | _cdUnknownExtraFields = null; |
| | 0 | 194 | | _lhUnknownExtraFields = null; |
| | | 195 | | |
| | 0 | 196 | | _fileComment = Array.Empty<byte>(); |
| | | 197 | | |
| | 0 | 198 | | if (_storedEntryNameBytes.Length > ushort.MaxValue) |
| | 0 | 199 | | { |
| | 0 | 200 | | throw new ArgumentException(SR.EntryNamesTooLong); |
| | | 201 | | } |
| | | 202 | | |
| | | 203 | | // grab the stream if we're in create mode |
| | 0 | 204 | | if (_archive.Mode == ZipArchiveMode.Create) |
| | 0 | 205 | | { |
| | 0 | 206 | | _archive.AcquireArchiveStream(this); |
| | 0 | 207 | | } |
| | | 208 | | |
| | 0 | 209 | | Changes = ZipArchive.ChangeState.Unchanged; |
| | 0 | 210 | | } |
| | | 211 | | |
| | | 212 | | /// <summary> |
| | | 213 | | /// The ZipArchive that this entry belongs to. If this entry has been deleted, this will return null. |
| | | 214 | | /// </summary> |
| | 0 | 215 | | public ZipArchive Archive => _archive; |
| | | 216 | | |
| | | 217 | | [CLSCompliant(false)] |
| | 126 | 218 | | public uint Crc32 => _crc32; |
| | | 219 | | |
| | | 220 | | /// <summary> |
| | | 221 | | /// Gets a value that indicates whether the entry is encrypted. |
| | | 222 | | /// </summary> |
| | 88708 | 223 | | public bool IsEncrypted => (_generalPurposeBitFlag & BitFlagValues.IsEncrypted) != 0; |
| | | 224 | | |
| | | 225 | | /// <summary> |
| | | 226 | | /// Gets the encryption method used to encrypt the entry. |
| | | 227 | | /// </summary> |
| | | 228 | | /// <value> |
| | | 229 | | /// <see cref="ZipEncryptionMethod.None"/> if the entry is not encrypted; |
| | | 230 | | /// <see cref="ZipEncryptionMethod.Unknown"/> if the entry uses an unsupported encryption method; |
| | | 231 | | /// otherwise, the specific encryption method (e.g., <see cref="ZipEncryptionMethod.ZipCrypto"/>, |
| | | 232 | | /// <see cref="ZipEncryptionMethod.Aes128"/>, <see cref="ZipEncryptionMethod.Aes192"/>, |
| | | 233 | | /// or <see cref="ZipEncryptionMethod.Aes256"/>). |
| | | 234 | | /// </value> |
| | 0 | 235 | | public ZipEncryptionMethod EncryptionMethod => _encryptionMethod; |
| | | 236 | | |
| | | 237 | | /// <summary> |
| | | 238 | | /// Gets the compression method used to compress the entry. |
| | | 239 | | /// </summary> |
| | | 240 | | public ZipCompressionMethod CompressionMethod |
| | | 241 | | { |
| | 29756 | 242 | | get => _storedCompressionMethod; |
| | | 243 | | private set |
| | 29756 | 244 | | { |
| | 29756 | 245 | | if (value == ZipCompressionMethod.Deflate) |
| | 278 | 246 | | { |
| | 278 | 247 | | VersionToExtractAtLeast(ZipVersionNeededValues.Deflate); |
| | 278 | 248 | | } |
| | 29478 | 249 | | else if (value == ZipCompressionMethod.Deflate64) |
| | 7360 | 250 | | { |
| | 7360 | 251 | | VersionToExtractAtLeast(ZipVersionNeededValues.Deflate64); |
| | 7360 | 252 | | } |
| | 29756 | 253 | | _storedCompressionMethod = value; |
| | 29756 | 254 | | } |
| | | 255 | | } |
| | | 256 | | |
| | | 257 | | /// <summary> |
| | | 258 | | /// The compressed size of the entry. If the archive that the entry belongs to is in Create mode, attempts to ge |
| | | 259 | | /// </summary> |
| | | 260 | | /// <exception cref="InvalidOperationException">This property is not available because the entry has been writte |
| | | 261 | | public long CompressedLength |
| | | 262 | | { |
| | | 263 | | get |
| | 0 | 264 | | { |
| | 0 | 265 | | if (_everOpenedForWrite) |
| | 0 | 266 | | { |
| | 0 | 267 | | throw new InvalidOperationException(SR.LengthAfterWrite); |
| | | 268 | | } |
| | 0 | 269 | | return _compressedSize; |
| | 0 | 270 | | } |
| | | 271 | | } |
| | | 272 | | |
| | | 273 | | public int ExternalAttributes |
| | | 274 | | { |
| | | 275 | | get |
| | 0 | 276 | | { |
| | 0 | 277 | | return (int)_externalFileAttr; |
| | 0 | 278 | | } |
| | | 279 | | set |
| | 0 | 280 | | { |
| | 0 | 281 | | ThrowIfInvalidArchive(); |
| | 0 | 282 | | _externalFileAttr = (uint)value; |
| | 0 | 283 | | Changes |= ZipArchive.ChangeState.FixedLengthMetadata; |
| | 0 | 284 | | } |
| | | 285 | | } |
| | | 286 | | |
| | | 287 | | /// <summary> |
| | | 288 | | /// Gets or sets the optional entry comment. |
| | | 289 | | /// </summary> |
| | | 290 | | /// <remarks> |
| | | 291 | | ///The comment encoding is determined by the <c>entryNameEncoding</c> parameter of the <see cref="ZipArchive(Str |
| | | 292 | | /// If the comment byte length is larger than <see cref="ushort.MaxValue"/>, it will be truncated when disposing |
| | | 293 | | /// </remarks> |
| | | 294 | | [AllowNull] |
| | | 295 | | public string Comment |
| | | 296 | | { |
| | 126 | 297 | | get => DecodeEntryString(_fileComment); |
| | | 298 | | set |
| | 0 | 299 | | { |
| | 0 | 300 | | _fileComment = ZipHelper.GetEncodedTruncatedBytesFromString(value, _archive.EntryNameAndCommentEncoding, |
| | | 301 | | |
| | 0 | 302 | | if (isUTF8) |
| | 0 | 303 | | { |
| | 0 | 304 | | _generalPurposeBitFlag |= BitFlagValues.UnicodeFileNameAndComment; |
| | 0 | 305 | | } |
| | 0 | 306 | | Changes |= ZipArchive.ChangeState.DynamicLengthMetadata; |
| | 0 | 307 | | } |
| | | 308 | | } |
| | | 309 | | |
| | | 310 | | /// <summary> |
| | | 311 | | /// The relative path of the entry as stored in the Zip archive. Note that Zip archives allow any string to be t |
| | | 312 | | /// </summary> |
| | | 313 | | public string FullName |
| | | 314 | | { |
| | | 315 | | get |
| | 29882 | 316 | | { |
| | 29882 | 317 | | return _storedEntryName; |
| | 29882 | 318 | | } |
| | | 319 | | |
| | | 320 | | [MemberNotNull(nameof(_storedEntryNameBytes))] |
| | | 321 | | [MemberNotNull(nameof(_storedEntryName))] |
| | | 322 | | private set |
| | 0 | 323 | | { |
| | 0 | 324 | | ArgumentNullException.ThrowIfNull(value, nameof(FullName)); |
| | | 325 | | |
| | 0 | 326 | | _storedEntryNameBytes = ZipHelper.GetEncodedTruncatedBytesFromString( |
| | 0 | 327 | | value, _archive.EntryNameAndCommentEncoding, 0 /* No truncation */, out bool isUTF8); |
| | | 328 | | |
| | 0 | 329 | | _storedEntryName = value; |
| | | 330 | | |
| | 0 | 331 | | if (isUTF8) |
| | 0 | 332 | | { |
| | 0 | 333 | | _generalPurposeBitFlag |= BitFlagValues.UnicodeFileNameAndComment; |
| | 0 | 334 | | } |
| | | 335 | | else |
| | 0 | 336 | | { |
| | 0 | 337 | | _generalPurposeBitFlag &= ~BitFlagValues.UnicodeFileNameAndComment; |
| | 0 | 338 | | } |
| | | 339 | | |
| | 0 | 340 | | DetectEntryNameVersion(); |
| | 0 | 341 | | } |
| | | 342 | | } |
| | | 343 | | |
| | | 344 | | /// <summary> |
| | | 345 | | /// Returns the relative path of the entry in the Zip archive, equivalent to <see cref="FullName"/>. |
| | | 346 | | /// </summary> |
| | 0 | 347 | | public override string ToString() => FullName; |
| | | 348 | | |
| | | 349 | | /// <summary> |
| | | 350 | | /// The last write time of the entry as stored in the Zip archive. When setting this property, the DateTime will |
| | | 351 | | /// Zip timestamp format, which supports a resolution of two seconds. If the data in the last write time field i |
| | | 352 | | /// an indicator value of 1980 January 1 at midnight will be returned. |
| | | 353 | | /// </summary> |
| | | 354 | | /// <exception cref="NotSupportedException">An attempt to set this property was made, but the ZipArchive that th |
| | | 355 | | /// opened in read-only mode.</exception> |
| | | 356 | | /// <exception cref="ArgumentOutOfRangeException">An attempt was made to set this property to a value that canno |
| | | 357 | | /// Zip timestamp format. The earliest date/time that can be represented is 1980 January 1 0:00:00 (midnight), a |
| | | 358 | | /// that can be represented is 2107 December 31 23:59:58 (one second before midnight).</exception> |
| | | 359 | | public DateTimeOffset LastWriteTime |
| | | 360 | | { |
| | | 361 | | get |
| | 126 | 362 | | { |
| | 126 | 363 | | return _lastModified; |
| | 126 | 364 | | } |
| | | 365 | | set |
| | 0 | 366 | | { |
| | 0 | 367 | | ThrowIfInvalidArchive(); |
| | 0 | 368 | | if (_archive.Mode == ZipArchiveMode.Read) |
| | 0 | 369 | | { |
| | 0 | 370 | | throw new NotSupportedException(SR.ReadOnlyArchive); |
| | | 371 | | } |
| | 0 | 372 | | if (_archive.Mode == ZipArchiveMode.Create && _everOpenedForWrite) |
| | 0 | 373 | | { |
| | 0 | 374 | | throw new IOException(SR.FrozenAfterWrite); |
| | | 375 | | } |
| | 0 | 376 | | if (value.DateTime.Year < ZipHelper.ValidZipDate_YearMin || value.DateTime.Year > ZipHelper.ValidZipDate |
| | 0 | 377 | | { |
| | 0 | 378 | | throw new ArgumentOutOfRangeException(nameof(value), SR.DateTimeOutOfRange); |
| | | 379 | | } |
| | | 380 | | |
| | 0 | 381 | | _lastModified = value; |
| | 0 | 382 | | Changes |= ZipArchive.ChangeState.FixedLengthMetadata; |
| | 0 | 383 | | } |
| | | 384 | | } |
| | | 385 | | |
| | | 386 | | /// <summary> |
| | | 387 | | /// The uncompressed size of the entry. This property is not valid in Create mode, and it is only valid in Updat |
| | | 388 | | /// </summary> |
| | | 389 | | /// <exception cref="InvalidOperationException">This property is not available because the entry has been writte |
| | | 390 | | public long Length |
| | | 391 | | { |
| | | 392 | | get |
| | 126 | 393 | | { |
| | 126 | 394 | | if (_everOpenedForWrite) |
| | 0 | 395 | | { |
| | 0 | 396 | | throw new InvalidOperationException(SR.LengthAfterWrite); |
| | | 397 | | } |
| | 126 | 398 | | return _uncompressedSize; |
| | 126 | 399 | | } |
| | | 400 | | } |
| | | 401 | | |
| | | 402 | | /// <summary> |
| | | 403 | | /// The filename of the entry. This is equivalent to the substring of Fullname that follows the final directory |
| | | 404 | | /// </summary> |
| | 0 | 405 | | public string Name => ParseFileName(FullName, _versionMadeByPlatform); |
| | | 406 | | |
| | | 407 | | /// <summary> |
| | | 408 | | /// Gets the "version made by" field of the entry as stored in the archive's central directory record. |
| | | 409 | | /// </summary> |
| | | 410 | | /// <remarks> |
| | | 411 | | /// As defined by the ZIP file format specification, the low byte contains the version of the specification used |
| | | 412 | | /// </remarks> |
| | | 413 | | [CLSCompliant(false)] |
| | 0 | 414 | | public ushort VersionMadeBy => (ushort)(((byte)_versionMadeByPlatform << 8) | (byte)_versionMadeBySpecification) |
| | | 415 | | |
| | 99204 | 416 | | internal ZipArchive.ChangeState Changes { get; private set; } |
| | | 417 | | |
| | 0 | 418 | | internal bool OriginallyInArchive => _originallyInArchive; |
| | | 419 | | |
| | 0 | 420 | | internal long OffsetOfLocalHeader => _offsetOfLocalHeader; |
| | | 421 | | |
| | | 422 | | /// <summary> |
| | | 423 | | /// Deletes the entry from the archive. |
| | | 424 | | /// </summary> |
| | | 425 | | /// <exception cref="IOException">The entry is already open for reading or writing.</exception> |
| | | 426 | | /// <exception cref="NotSupportedException">The ZipArchive that this entry belongs to was opened in a mode other |
| | | 427 | | /// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exce |
| | | 428 | | public void Delete() |
| | 0 | 429 | | { |
| | 0 | 430 | | if (_archive == null) |
| | 0 | 431 | | { |
| | 0 | 432 | | return; |
| | | 433 | | } |
| | | 434 | | |
| | 0 | 435 | | if (_currentlyOpenForWrite) |
| | 0 | 436 | | { |
| | 0 | 437 | | throw new IOException(SR.DeleteOpenEntry); |
| | | 438 | | } |
| | | 439 | | |
| | 0 | 440 | | if (_archive.Mode != ZipArchiveMode.Update) |
| | 0 | 441 | | { |
| | 0 | 442 | | throw new NotSupportedException(SR.DeleteOnlyInUpdate); |
| | | 443 | | } |
| | | 444 | | |
| | 0 | 445 | | _archive.ThrowIfDisposed(); |
| | | 446 | | |
| | 0 | 447 | | _archive.RemoveEntry(this); |
| | 0 | 448 | | _archive = null!; |
| | 0 | 449 | | UnloadStreams(); |
| | 0 | 450 | | } |
| | | 451 | | |
| | | 452 | | /// <summary> |
| | | 453 | | /// Opens the entry. If the archive that the entry belongs to was opened in Read mode, the returned stream will |
| | | 454 | | /// </summary> |
| | | 455 | | /// <returns>A Stream that represents the contents of the entry.</returns> |
| | | 456 | | /// <exception cref="IOException">The entry is already currently open for writing. -or- The entry has been delet |
| | | 457 | | /// <exception cref="InvalidDataException">The entry is missing from the archive or is corrupt and cannot be rea |
| | | 458 | | /// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exce |
| | | 459 | | public Stream Open() |
| | 0 | 460 | | { |
| | 0 | 461 | | ThrowIfInvalidArchive(); |
| | 0 | 462 | | return OpenCore(InferAccessFromMode()); |
| | 0 | 463 | | } |
| | | 464 | | |
| | | 465 | | |
| | | 466 | | /// <summary> |
| | | 467 | | /// Opens the entry for reading or updating with the specified password. |
| | | 468 | | /// If the entry is not encrypted, the password is ignored and the entry is opened normally. |
| | | 469 | | /// </summary> |
| | | 470 | | /// <returns>A Stream that represents the contents of the entry.</returns> |
| | | 471 | | /// <exception cref="IOException">The entry is already currently open for writing. -or- The entry has been delet |
| | | 472 | | /// <exception cref="InvalidDataException">The entry is missing from the archive or is corrupt and cannot be rea |
| | | 473 | | /// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exce |
| | | 474 | | /// <exception cref="InvalidOperationException">The requested access is not compatible with the archive's open m |
| | | 475 | | /// <param name="password">The password used to decrypt the entry. If the entry is not encrypted, this parameter |
| | | 476 | | public Stream Open(ReadOnlySpan<char> password) |
| | 0 | 477 | | { |
| | 0 | 478 | | ThrowIfInvalidArchive(); |
| | | 479 | | |
| | 0 | 480 | | if (IsEncrypted && password.IsEmpty) |
| | 0 | 481 | | { |
| | 0 | 482 | | throw new ArgumentException(SR.PasswordRequired, nameof(password)); |
| | | 483 | | } |
| | | 484 | | |
| | 0 | 485 | | return OpenCore(InferAccessFromMode(), password); |
| | 0 | 486 | | } |
| | | 487 | | |
| | | 488 | | /// <summary> |
| | | 489 | | /// Opens the entry with the specified access mode. This allows for more granular control over the returned stre |
| | | 490 | | /// </summary> |
| | | 491 | | /// <param name="access">The file access mode for the returned stream.</param> |
| | | 492 | | /// <returns>A <see cref="Stream"/> that represents the contents of the entry with the specified access capabili |
| | | 493 | | /// <remarks> |
| | | 494 | | /// <para>The allowed <paramref name="access"/> values depend on the <see cref="ZipArchiveMode"/>:</para> |
| | | 495 | | /// <list type="bullet"> |
| | | 496 | | /// <item><description><see cref="ZipArchiveMode.Read"/>: Only <see cref="FileAccess.Read"/> is allowed.</descri |
| | | 497 | | /// <item><description><see cref="ZipArchiveMode.Create"/>: <see cref="FileAccess.Write"/> and <see cref="FileAc |
| | | 498 | | /// <item><description><see cref="ZipArchiveMode.Update"/>: All values are allowed. <see cref="FileAccess.Read"/ |
| | | 499 | | /// </list> |
| | | 500 | | /// </remarks> |
| | | 501 | | /// <exception cref="ArgumentOutOfRangeException"><paramref name="access"/> is not a valid <see cref="FileAccess |
| | | 502 | | /// <exception cref="InvalidOperationException">The requested access is not compatible with the archive's open m |
| | | 503 | | /// <exception cref="IOException">The entry is already currently open for writing. -or- The entry has been delet |
| | | 504 | | /// <exception cref="InvalidDataException">The entry is missing from the archive or is corrupt and cannot be rea |
| | | 505 | | /// <exception cref="ObjectDisposedException">The ZipArchive that this entry belongs to has been disposed.</exce |
| | | 506 | | public Stream Open(FileAccess access) |
| | 0 | 507 | | { |
| | 0 | 508 | | ThrowIfInvalidArchive(); |
| | 0 | 509 | | ValidateAccessForMode(access); |
| | 0 | 510 | | return OpenCore(access); |
| | 0 | 511 | | } |
| | | 512 | | |
| | | 513 | | public Stream Open(FileAccess access, ReadOnlySpan<char> password) |
| | 0 | 514 | | { |
| | 0 | 515 | | ThrowIfInvalidArchive(); |
| | 0 | 516 | | ValidateAccessForMode(access); |
| | | 517 | | |
| | 0 | 518 | | if (IsEncrypted && password.IsEmpty) |
| | 0 | 519 | | { |
| | 0 | 520 | | throw new ArgumentException(SR.PasswordRequired, nameof(password)); |
| | | 521 | | } |
| | | 522 | | |
| | 0 | 523 | | return OpenCore(access, password); |
| | 0 | 524 | | } |
| | | 525 | | |
| | 0 | 526 | | private FileAccess InferAccessFromMode() => _archive.Mode switch |
| | 0 | 527 | | { |
| | 0 | 528 | | ZipArchiveMode.Read => FileAccess.Read, |
| | 0 | 529 | | ZipArchiveMode.Create => FileAccess.Write, |
| | 0 | 530 | | _ => FileAccess.ReadWrite |
| | 0 | 531 | | }; |
| | | 532 | | |
| | | 533 | | private void ValidateAccessForMode(FileAccess access) |
| | 0 | 534 | | { |
| | 0 | 535 | | if (access is not (FileAccess.Read or FileAccess.Write or FileAccess.ReadWrite)) |
| | 0 | 536 | | { |
| | 0 | 537 | | throw new ArgumentOutOfRangeException(nameof(access), SR.InvalidFileAccess); |
| | | 538 | | } |
| | | 539 | | |
| | 0 | 540 | | switch (_archive.Mode) |
| | | 541 | | { |
| | | 542 | | case ZipArchiveMode.Read: |
| | 0 | 543 | | if (access != FileAccess.Read) |
| | 0 | 544 | | { |
| | 0 | 545 | | throw new InvalidOperationException(SR.CannotBeWrittenInReadMode); |
| | | 546 | | } |
| | 0 | 547 | | break; |
| | | 548 | | case ZipArchiveMode.Create: |
| | 0 | 549 | | if (access == FileAccess.Read) |
| | 0 | 550 | | { |
| | 0 | 551 | | throw new InvalidOperationException(SR.CannotBeReadInCreateMode); |
| | | 552 | | } |
| | 0 | 553 | | break; |
| | | 554 | | } |
| | 0 | 555 | | } |
| | | 556 | | |
| | | 557 | | private Stream OpenCore(FileAccess access, ReadOnlySpan<char> password = default) |
| | 0 | 558 | | { |
| | 0 | 559 | | switch (_archive.Mode) |
| | | 560 | | { |
| | | 561 | | case ZipArchiveMode.Read: |
| | 0 | 562 | | return OpenInReadMode(checkOpenable: true, password); |
| | | 563 | | case ZipArchiveMode.Create: |
| | 0 | 564 | | return OpenInWriteMode(); |
| | | 565 | | case ZipArchiveMode.Update: |
| | | 566 | | default: |
| | 0 | 567 | | Debug.Assert(_archive.Mode == ZipArchiveMode.Update); |
| | 0 | 568 | | return access switch |
| | 0 | 569 | | { |
| | 0 | 570 | | // Reads in Update mode must observe content written earlier in this session and |
| | 0 | 571 | | // treat a newly created entry as empty. Only an unmodified entry that already |
| | 0 | 572 | | // exists in the archive can be read directly without loading it into memory. |
| | 0 | 573 | | FileAccess.Read => _storedUncompressedData is not null || !_originallyInArchive |
| | 0 | 574 | | ? OpenInUpdateModeForRead() |
| | 0 | 575 | | : OpenInReadMode(checkOpenable: true, password), |
| | 0 | 576 | | FileAccess.Write => OpenInUpdateMode(loadExistingContent: false, password), |
| | 0 | 577 | | _ => OpenInUpdateMode(loadExistingContent: true, password), |
| | 0 | 578 | | }; |
| | | 579 | | } |
| | 0 | 580 | | } |
| | | 581 | | |
| | | 582 | | private string DecodeEntryString(byte[] entryStringBytes) |
| | 29882 | 583 | | { |
| | 29882 | 584 | | Debug.Assert(entryStringBytes != null); |
| | | 585 | | |
| | 29882 | 586 | | Encoding readEntryStringEncoding = |
| | 29882 | 587 | | (_generalPurposeBitFlag & BitFlagValues.UnicodeFileNameAndComment) == BitFlagValues.UnicodeFileNameAndCo |
| | 29882 | 588 | | ? Encoding.UTF8 |
| | 29882 | 589 | | : _archive?.EntryNameAndCommentEncoding ?? Encoding.UTF8; |
| | | 590 | | |
| | 29882 | 591 | | return readEntryStringEncoding.GetString(entryStringBytes); |
| | 29882 | 592 | | } |
| | | 593 | | |
| | | 594 | | // Only allow opening ZipArchives with large ZipArchiveEntries in update mode when running in a 64-bit process. |
| | | 595 | | // This is for compatibility with old behavior that threw an exception for all process bitnesses, because this |
| | | 596 | | // will not work in a 32-bit process. |
| | 0 | 597 | | private static readonly bool s_allowLargeZipArchiveEntriesInUpdateMode = IntPtr.Size > 4; |
| | | 598 | | |
| | 0 | 599 | | internal bool EverOpenedForWrite => _everOpenedForWrite; |
| | | 600 | | |
| | | 601 | | internal long GetOffsetOfCompressedData() |
| | 403 | 602 | | { |
| | 403 | 603 | | if (_storedOffsetOfCompressedData == null) |
| | 403 | 604 | | { |
| | | 605 | | // Seek to local header |
| | 403 | 606 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); |
| | | 607 | | |
| | | 608 | | // Skip the local file header to get to the compressed data |
| | | 609 | | // TrySkipBlock handles both AES and non-AES cases correctly |
| | 403 | 610 | | if (!ZipLocalFileHeader.TrySkipBlock(_archive.ArchiveStream)) |
| | 383 | 611 | | { |
| | 383 | 612 | | throw new InvalidDataException(SR.LocalFileHeaderCorrupt); |
| | | 613 | | } |
| | | 614 | | |
| | 20 | 615 | | _storedOffsetOfCompressedData = _archive.ArchiveStream.Position; |
| | 20 | 616 | | } |
| | | 617 | | |
| | 20 | 618 | | return _storedOffsetOfCompressedData.Value; |
| | 20 | 619 | | } |
| | | 620 | | |
| | | 621 | | /// <summary> |
| | | 622 | | /// Reads and caches the AES encryption salt from the local file data area. |
| | | 623 | | /// Called during central directory parsing for AES-encrypted entries so that |
| | | 624 | | /// the salt is available for key derivation without additional I/O at open time. |
| | | 625 | | /// </summary> |
| | | 626 | | internal void ReadEncryptionSaltIfNeeded() |
| | 903 | 627 | | { |
| | 903 | 628 | | if (!IsAesEncrypted || !_originallyInArchive || OperatingSystem.IsBrowser()) |
| | 345 | 629 | | { |
| | 345 | 630 | | return; |
| | | 631 | | } |
| | | 632 | | |
| | | 633 | | // A corrupt central directory can point the local header offset past the end of the |
| | | 634 | | // archive. Seeking there throws ArgumentOutOfRangeException on some streams (e.g. |
| | | 635 | | // MemoryStream). Mirror the check in IsOpenableInitialVerifications and defer the error |
| | | 636 | | // to when the entry is actually opened, same as for non AES encrypted entries. |
| | 558 | 637 | | if (_offsetOfLocalHeader > _archive.ArchiveStream.Length) |
| | 155 | 638 | | { |
| | 155 | 639 | | return; |
| | | 640 | | } |
| | | 641 | | |
| | 403 | 642 | | long savedPosition = _archive.ArchiveStream.Position; |
| | | 643 | | try |
| | 403 | 644 | | { |
| | 403 | 645 | | long offset = GetOffsetOfCompressedData(); |
| | 20 | 646 | | _archive.ArchiveStream.Seek(offset, SeekOrigin.Begin); |
| | | 647 | | |
| | 20 | 648 | | int keySizeBits = GetAesKeySizeBits(Encryption); |
| | 0 | 649 | | int saltSize = WinZipAesStream.GetSaltSize(keySizeBits); |
| | 0 | 650 | | _aesSalt = new byte[saltSize]; |
| | 0 | 651 | | _archive.ArchiveStream.ReadExactly(_aesSalt); |
| | 0 | 652 | | } |
| | 403 | 653 | | catch (Exception ex) when (ex is InvalidDataException or EndOfStreamException) |
| | 403 | 654 | | { |
| | | 655 | | // These are the only exceptions GetOffsetOfCompressedData() and ReadExactly() |
| | | 656 | | // can throw for corrupt or truncated data. Swallow them here and defer the error |
| | | 657 | | // to when the entry is actually opened. |
| | 403 | 658 | | _aesSalt = null; |
| | 403 | 659 | | } |
| | | 660 | | finally |
| | 403 | 661 | | { |
| | 403 | 662 | | _archive.ArchiveStream.Seek(savedPosition, SeekOrigin.Begin); |
| | 403 | 663 | | } |
| | 903 | 664 | | } |
| | | 665 | | |
| | | 666 | | private MemoryStream GetUncompressedData(ReadOnlySpan<char> password = default) |
| | 0 | 667 | | { |
| | 0 | 668 | | if (_storedUncompressedData == null) |
| | 0 | 669 | | { |
| | | 670 | | // this means we have never opened it before |
| | | 671 | | |
| | | 672 | | |
| | | 673 | | // MemoryStream is backed by a single byte[] and cannot grow beyond Array.MaxLength. |
| | | 674 | | // Validate up front before attempting the (int) cast. |
| | 0 | 675 | | if ((ulong)_uncompressedSize > (ulong)Array.MaxLength) |
| | 0 | 676 | | { |
| | 0 | 677 | | _currentlyOpenForWrite = false; |
| | 0 | 678 | | throw new InvalidDataException(SR.EntryUncompressedSizeTooLargeForUpdateMode); |
| | | 679 | | } |
| | | 680 | | |
| | 0 | 681 | | _storedUncompressedData = new MemoryStream((int)_uncompressedSize); |
| | | 682 | | |
| | 0 | 683 | | if (_originallyInArchive) |
| | 0 | 684 | | { |
| | 0 | 685 | | Stream decompressor = !password.IsEmpty |
| | 0 | 686 | | ? OpenInReadMode(checkOpenable: false, password) |
| | 0 | 687 | | : OpenInReadMode(checkOpenable: false); |
| | | 688 | | |
| | 0 | 689 | | using (decompressor) |
| | 0 | 690 | | { |
| | | 691 | | try |
| | 0 | 692 | | { |
| | 0 | 693 | | decompressor.CopyTo(_storedUncompressedData); |
| | 0 | 694 | | } |
| | 0 | 695 | | catch (InvalidDataException) |
| | 0 | 696 | | { |
| | | 697 | | // this is the case where the archive say the entry is deflate, but deflateStream |
| | | 698 | | // throws an InvalidDataException. This property should only be getting accessed in |
| | | 699 | | // Update mode, so we want to make sure _storedUncompressedData stays null so |
| | | 700 | | // that later when we dispose the archive, this entry loads the compressedBytes, and |
| | | 701 | | // copies them straight over |
| | 0 | 702 | | _storedUncompressedData.Dispose(); |
| | 0 | 703 | | _storedUncompressedData = null; |
| | 0 | 704 | | _currentlyOpenForWrite = false; |
| | 0 | 705 | | _everOpenedForWrite = false; |
| | 0 | 706 | | _derivedZipCryptoKeyMaterial = null; |
| | 0 | 707 | | _derivedAesKeyMaterial = null; |
| | 0 | 708 | | throw; |
| | | 709 | | } |
| | 0 | 710 | | } |
| | 0 | 711 | | } |
| | | 712 | | |
| | | 713 | | // NOTE: CompressionMethod normalization is deferred to MarkAsModified() to avoid |
| | | 714 | | // corrupting entries that are opened in Update mode but not actually written to. |
| | | 715 | | // If we normalized here and the entry wasn't modified, we'd write a header with |
| | | 716 | | // CompressionMethod=Deflate but the original _compressedBytes would still be in |
| | | 717 | | // their original format (e.g., Deflate64), producing an invalid entry. |
| | 0 | 718 | | } |
| | | 719 | | |
| | 0 | 720 | | return _storedUncompressedData; |
| | 0 | 721 | | } |
| | | 722 | | // does almost everything you need to do to forget about this entry |
| | | 723 | | // writes the local header/data, gets rid of all the data, |
| | | 724 | | // closes all of the streams except for the very outermost one that |
| | | 725 | | // the user holds on to and is responsible for closing |
| | | 726 | | // |
| | | 727 | | // after calling this, and only after calling this can we be guaranteed |
| | | 728 | | // that we are reading to write the central directory |
| | | 729 | | // |
| | | 730 | | // should only throw an exception in extremely exceptional cases because it is called from dispose |
| | | 731 | | internal void WriteAndFinishLocalEntry(bool forceWrite) |
| | 0 | 732 | | { |
| | 0 | 733 | | CloseStreams(); |
| | 0 | 734 | | WriteLocalFileHeaderAndDataIfNeeded(forceWrite); |
| | 0 | 735 | | UnloadStreams(); |
| | 0 | 736 | | } |
| | | 737 | | |
| | | 738 | | private bool WriteCentralDirectoryFileHeaderInitialize(bool forceWrite, out Zip64ExtraField? zip64ExtraField, ou |
| | 0 | 739 | | { |
| | | 740 | | // This part is simple, because we should definitely know the sizes by this time |
| | | 741 | | |
| | | 742 | | // _storedEntryNameBytes only gets set when we read in or call moveTo. MoveTo does a check, and |
| | | 743 | | // reading in should not be able to produce an entryname longer than ushort.MaxValue |
| | | 744 | | // _fileComment only gets set when we read in or set the FileComment property. This performs its own |
| | | 745 | | // length check. |
| | 0 | 746 | | Debug.Assert(_storedEntryNameBytes.Length <= ushort.MaxValue); |
| | 0 | 747 | | Debug.Assert(_fileComment.Length <= ushort.MaxValue); |
| | | 748 | | |
| | | 749 | | // decide if we need the Zip64 extra field: |
| | 0 | 750 | | zip64ExtraField = null; |
| | | 751 | | |
| | 0 | 752 | | if (AreSizesTooLarge |
| | 0 | 753 | | #if DEBUG_FORCE_ZIP64 |
| | 0 | 754 | | || _archive._forceZip64 |
| | 0 | 755 | | #endif |
| | 0 | 756 | | ) |
| | 0 | 757 | | { |
| | 0 | 758 | | compressedSizeTruncated = ZipHelper.Mask32Bit; |
| | 0 | 759 | | uncompressedSizeTruncated = ZipHelper.Mask32Bit; |
| | | 760 | | |
| | | 761 | | // If we have one of the sizes, the other must go in there as speced for LH, but not necessarily for CH, |
| | 0 | 762 | | zip64ExtraField = new() |
| | 0 | 763 | | { |
| | 0 | 764 | | CompressedSize = _compressedSize, |
| | 0 | 765 | | UncompressedSize = _uncompressedSize |
| | 0 | 766 | | }; |
| | 0 | 767 | | } |
| | | 768 | | else |
| | 0 | 769 | | { |
| | 0 | 770 | | compressedSizeTruncated = (uint)_compressedSize; |
| | 0 | 771 | | uncompressedSizeTruncated = (uint)_uncompressedSize; |
| | 0 | 772 | | } |
| | | 773 | | |
| | | 774 | | |
| | 0 | 775 | | if (IsOffsetTooLarge |
| | 0 | 776 | | #if DEBUG_FORCE_ZIP64 |
| | 0 | 777 | | || _archive._forceZip64 |
| | 0 | 778 | | #endif |
| | 0 | 779 | | ) |
| | 0 | 780 | | { |
| | 0 | 781 | | offsetOfLocalHeaderTruncated = ZipHelper.Mask32Bit; |
| | | 782 | | |
| | | 783 | | // If we already created a zip64 extra field for sizes, add the offset to it |
| | | 784 | | // Otherwise, create a new one for just the offset |
| | 0 | 785 | | zip64ExtraField ??= new(); |
| | 0 | 786 | | zip64ExtraField.LocalHeaderOffset = _offsetOfLocalHeader; |
| | 0 | 787 | | } |
| | | 788 | | else |
| | 0 | 789 | | { |
| | 0 | 790 | | offsetOfLocalHeaderTruncated = (uint)_offsetOfLocalHeader; |
| | 0 | 791 | | } |
| | | 792 | | |
| | 0 | 793 | | if (zip64ExtraField != null) |
| | 0 | 794 | | { |
| | 0 | 795 | | VersionToExtractAtLeast(ZipVersionNeededValues.Zip64); |
| | 0 | 796 | | } |
| | | 797 | | |
| | | 798 | | |
| | 0 | 799 | | WinZipAesExtraField? aesExtraField = null; |
| | 0 | 800 | | int aesExtraFieldSize = 0; |
| | | 801 | | |
| | 0 | 802 | | if (UseAesEncryption) |
| | 0 | 803 | | { |
| | 0 | 804 | | aesExtraField = CreateAesExtraField(); |
| | 0 | 805 | | aesExtraFieldSize = WinZipAesExtraField.TotalSize; |
| | 0 | 806 | | } |
| | | 807 | | |
| | | 808 | | // determine if we can fit zip64 extra field and original extra fields all in |
| | | 809 | | // When using AES encryption, exclude the AES tag from currExtraFieldDataLength since we're writing a new on |
| | 0 | 810 | | int currExtraFieldDataLength = UseAesEncryption |
| | 0 | 811 | | ? ZipGenericExtraField.TotalSizeExcludingTag(_cdUnknownExtraFields, _cdTrailingExtraFieldData?.Length ?? |
| | 0 | 812 | | : ZipGenericExtraField.TotalSize(_cdUnknownExtraFields, _cdTrailingExtraFieldData?.Length ?? 0); |
| | 0 | 813 | | int bigExtraFieldLength = (zip64ExtraField != null ? zip64ExtraField.TotalSize : 0) |
| | 0 | 814 | | + aesExtraFieldSize |
| | 0 | 815 | | + currExtraFieldDataLength; |
| | | 816 | | |
| | 0 | 817 | | if (bigExtraFieldLength > ushort.MaxValue) |
| | 0 | 818 | | { |
| | 0 | 819 | | extraFieldLength = (ushort)((zip64ExtraField != null ? zip64ExtraField.TotalSize : 0) + aesExtraFieldSiz |
| | 0 | 820 | | _cdUnknownExtraFields = null; |
| | 0 | 821 | | } |
| | | 822 | | else |
| | 0 | 823 | | { |
| | 0 | 824 | | extraFieldLength = (ushort)bigExtraFieldLength; |
| | 0 | 825 | | } |
| | | 826 | | |
| | 0 | 827 | | if (_originallyInArchive && Changes == ZipArchive.ChangeState.Unchanged && !forceWrite) |
| | 0 | 828 | | { |
| | 0 | 829 | | long centralDirectoryHeaderLength = ZipCentralDirectoryFileHeader.FieldLocations.DynamicData |
| | 0 | 830 | | + _storedEntryNameBytes.Length |
| | 0 | 831 | | + extraFieldLength |
| | 0 | 832 | | + _fileComment.Length; |
| | | 833 | | |
| | 0 | 834 | | _archive.ArchiveStream.Seek(centralDirectoryHeaderLength, SeekOrigin.Current); |
| | | 835 | | |
| | 0 | 836 | | return false; |
| | | 837 | | } |
| | | 838 | | |
| | 0 | 839 | | return true; |
| | 0 | 840 | | } |
| | | 841 | | |
| | | 842 | | private void WriteCentralDirectoryFileHeaderPrepare(Span<byte> cdStaticHeader, uint compressedSizeTruncated, uin |
| | 0 | 843 | | { |
| | | 844 | | // The central directory file header begins with the below constant-length structure: |
| | | 845 | | // Central directory file header signature (4 bytes) |
| | | 846 | | // Version made by Specification (version) (1 byte) |
| | | 847 | | // Version made by Compatibility (type) (1 byte) |
| | | 848 | | // Minimum version needed to extract (2 bytes) |
| | | 849 | | // General Purpose bit flag (2 bytes) |
| | | 850 | | // The Compression method (2 bytes) |
| | | 851 | | // File last modification time and date (4 bytes) |
| | | 852 | | // CRC-32 (4 bytes) |
| | | 853 | | // Compressed Size (4 bytes) |
| | | 854 | | // Uncompressed Size (4 bytes) |
| | | 855 | | // File Name Length (2 bytes) |
| | | 856 | | // Extra Field Length (2 bytes) |
| | | 857 | | // File Comment Length (2 bytes) |
| | | 858 | | // Start Disk Number (2 bytes) |
| | | 859 | | // Internal File Attributes (2 bytes) |
| | | 860 | | // External File Attributes (4 bytes) |
| | | 861 | | // Offset Of Local Header (4 bytes) |
| | | 862 | | |
| | 0 | 863 | | ZipCentralDirectoryFileHeader.SignatureConstantBytes.CopyTo(cdStaticHeader[ZipCentralDirectoryFileHeader.Fie |
| | 0 | 864 | | cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.VersionMadeBySpecification] = (byte)_versionMade |
| | 0 | 865 | | cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.VersionMadeByCompatibility] = (byte)CurrentZipPl |
| | 0 | 866 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Version |
| | 0 | 867 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.General |
| | | 868 | | |
| | | 869 | | // For AES encryption, write compression method 99 (Aes) in the header |
| | | 870 | | // _headerCompressionMethod preserves the original value from the central directory |
| | 0 | 871 | | ushort compressionMethodToWrite = UseAesEncryption ? (ushort)WinZipAesMethod : (ushort)CompressionMethod; |
| | 0 | 872 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Compres |
| | | 873 | | |
| | 0 | 874 | | BinaryPrimitives.WriteUInt32LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.LastMod |
| | | 875 | | // when using aes encryption, ae-2 standard dictates crc to be 0 |
| | 0 | 876 | | uint crcToWrite = UseAesEncryption ? 0 : _crc32; |
| | 0 | 877 | | BinaryPrimitives.WriteUInt32LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Crc32.. |
| | 0 | 878 | | BinaryPrimitives.WriteUInt32LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Compres |
| | 0 | 879 | | BinaryPrimitives.WriteUInt32LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Uncompr |
| | 0 | 880 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Filenam |
| | 0 | 881 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.ExtraFi |
| | 0 | 882 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.FileCom |
| | 0 | 883 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.DiskNum |
| | 0 | 884 | | BinaryPrimitives.WriteUInt16LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Interna |
| | 0 | 885 | | BinaryPrimitives.WriteUInt32LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Externa |
| | 0 | 886 | | BinaryPrimitives.WriteUInt32LittleEndian(cdStaticHeader[ZipCentralDirectoryFileHeader.FieldLocations.Relativ |
| | 0 | 887 | | } |
| | | 888 | | |
| | | 889 | | // should only throw an exception in extremely exceptional cases because it is called from dispose |
| | | 890 | | internal unsafe void WriteCentralDirectoryFileHeader(bool forceWrite) |
| | 0 | 891 | | { |
| | 0 | 892 | | if (WriteCentralDirectoryFileHeaderInitialize(forceWrite, out Zip64ExtraField? zip64ExtraField, out uint com |
| | 0 | 893 | | { |
| | 0 | 894 | | Span<byte> cdStaticHeader = stackalloc byte[ZipCentralDirectoryFileHeader.BlockConstantSectionSize]; |
| | 0 | 895 | | WriteCentralDirectoryFileHeaderPrepare(cdStaticHeader, compressedSizeTruncated, uncompressedSizeTruncate |
| | | 896 | | |
| | 0 | 897 | | _archive.ArchiveStream.Write(cdStaticHeader); |
| | 0 | 898 | | _archive.ArchiveStream.Write(_storedEntryNameBytes); |
| | | 899 | | |
| | | 900 | | // only write zip64ExtraField if we decided we need it (it's not null) |
| | 0 | 901 | | zip64ExtraField?.WriteBlock(_archive.ArchiveStream); |
| | | 902 | | |
| | | 903 | | // Write AES extra field if using AES encryption |
| | 0 | 904 | | if (UseAesEncryption) |
| | 0 | 905 | | { |
| | 0 | 906 | | CreateAesExtraField().WriteBlock(_archive.ArchiveStream); |
| | | 907 | | |
| | | 908 | | // write extra fields excluding existing AES extra field (and any malformed trailing data). |
| | 0 | 909 | | ZipGenericExtraField.WriteAllBlocksExcludingTag(_cdUnknownExtraFields, _cdTrailingExtraFieldData ?? |
| | 0 | 910 | | } |
| | | 911 | | else |
| | 0 | 912 | | { |
| | | 913 | | // write extra fields (and any malformed trailing data). |
| | 0 | 914 | | ZipGenericExtraField.WriteAllBlocks(_cdUnknownExtraFields, _cdTrailingExtraFieldData ?? Array.Empty< |
| | 0 | 915 | | } |
| | | 916 | | |
| | 0 | 917 | | if (_fileComment.Length > 0) |
| | 0 | 918 | | { |
| | 0 | 919 | | _archive.ArchiveStream.Write(_fileComment); |
| | 0 | 920 | | } |
| | 0 | 921 | | } |
| | 0 | 922 | | } |
| | | 923 | | |
| | | 924 | | // throws exception if fails, will get called on every relevant entry before closing in update mode |
| | | 925 | | // can throw InvalidDataException |
| | | 926 | | internal void LoadLocalHeaderExtraFieldIfNeeded() |
| | 0 | 927 | | { |
| | | 928 | | // we should have made this exact call in _archive.Init through ThrowIfOpenable |
| | 0 | 929 | | Debug.Assert(IsOpenable(false, true, out _)); |
| | | 930 | | |
| | | 931 | | // load local header's extra fields. it will be null if we couldn't read for some reason |
| | 0 | 932 | | if (_originallyInArchive) |
| | 0 | 933 | | { |
| | 0 | 934 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); |
| | 0 | 935 | | _lhUnknownExtraFields = ZipLocalFileHeader.GetExtraFields(_archive.ArchiveStream, out _lhTrailingExtraFi |
| | 0 | 936 | | } |
| | 0 | 937 | | } |
| | | 938 | | |
| | | 939 | | private byte[][] LoadCompressedBytesIfNeededInitialize(out int maxSingleBufferSize) |
| | 0 | 940 | | { |
| | | 941 | | // we know that it is openable at this point |
| | 0 | 942 | | maxSingleBufferSize = Array.MaxLength; |
| | | 943 | | |
| | 0 | 944 | | byte[][] compressedBytes = new byte[(_compressedSize / maxSingleBufferSize) + 1][]; |
| | 0 | 945 | | for (int i = 0; i < compressedBytes.Length - 1; i++) |
| | 0 | 946 | | { |
| | 0 | 947 | | compressedBytes[i] = new byte[maxSingleBufferSize]; |
| | 0 | 948 | | } |
| | 0 | 949 | | compressedBytes[compressedBytes.Length - 1] = new byte[_compressedSize % maxSingleBufferSize]; |
| | | 950 | | |
| | 0 | 951 | | return compressedBytes; |
| | 0 | 952 | | } |
| | | 953 | | |
| | | 954 | | // throws exception if fails, will get called on every relevant entry before closing in update mode |
| | | 955 | | // can throw InvalidDataException |
| | | 956 | | internal void LoadCompressedBytesIfNeeded() |
| | 0 | 957 | | { |
| | | 958 | | // we should have made this exact call in _archive.Init through ThrowIfOpenable |
| | 0 | 959 | | Debug.Assert(IsOpenable(false, true, out _)); |
| | | 960 | | |
| | 0 | 961 | | if (!_everOpenedForWrite && _originallyInArchive) |
| | 0 | 962 | | { |
| | 0 | 963 | | _compressedBytes = LoadCompressedBytesIfNeededInitialize(out int maxSingleBufferSize); |
| | | 964 | | |
| | 0 | 965 | | _archive.ArchiveStream.Seek(GetOffsetOfCompressedData(), SeekOrigin.Begin); |
| | | 966 | | |
| | 0 | 967 | | for (int i = 0; i < _compressedBytes.Length - 1; i++) |
| | 0 | 968 | | { |
| | 0 | 969 | | _archive.ArchiveStream.ReadAtLeast(_compressedBytes[i], maxSingleBufferSize, throwOnEndOfStream: tru |
| | 0 | 970 | | } |
| | 0 | 971 | | _archive.ArchiveStream.ReadAtLeast(_compressedBytes[_compressedBytes.Length - 1], (int)(_compressedSize |
| | 0 | 972 | | } |
| | 0 | 973 | | } |
| | | 974 | | |
| | | 975 | | internal void ThrowIfNotOpenable(bool needToUncompress, bool needToLoadIntoMemory) |
| | 0 | 976 | | { |
| | 0 | 977 | | if (!IsOpenable(needToUncompress, needToLoadIntoMemory, out string? message)) |
| | 0 | 978 | | { |
| | 0 | 979 | | throw new InvalidDataException(message); |
| | | 980 | | } |
| | 0 | 981 | | } |
| | | 982 | | |
| | | 983 | | private void DetectEntryNameVersion() |
| | 29756 | 984 | | { |
| | 29756 | 985 | | if (ParseFileName(_storedEntryName, _versionMadeByPlatform) == "") |
| | 28982 | 986 | | { |
| | 28982 | 987 | | VersionToExtractAtLeast(ZipVersionNeededValues.ExplicitDirectory); |
| | 28982 | 988 | | } |
| | 29756 | 989 | | } |
| | | 990 | | |
| | | 991 | | |
| | | 992 | | private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, bool leaveBackingStreamOpen, EventHan |
| | 0 | 993 | | { |
| | | 994 | | // stream stack: backingStream -> DeflateStream -> CheckSumWriteStream |
| | | 995 | | |
| | | 996 | | // By default we compress with deflate, except if compression level |
| | | 997 | | // is set to NoCompression then stored is used. |
| | | 998 | | // |
| | | 999 | | // Stored is also used for empty files, but we can't know at this |
| | | 1000 | | // point if user will write anything to the stream or not. For that |
| | | 1001 | | // reason, we defer the instantiation of the compression stream |
| | | 1002 | | // until the first write to the CheckSumAndSizeWriteStream happens. |
| | | 1003 | | // If the user never writes anything, this will be detected during |
| | | 1004 | | // saving and the compression method in the file header will be |
| | | 1005 | | // changed to Stored. |
| | | 1006 | | // |
| | | 1007 | | // Note: Deflate64 is not supported on all platforms |
| | 0 | 1008 | | Debug.Assert(CompressionMethod == ZipCompressionMethod.Deflate |
| | 0 | 1009 | | || CompressionMethod == ZipCompressionMethod.Stored); |
| | | 1010 | | Func<Stream> compressorStreamFactory; |
| | | 1011 | | |
| | 0 | 1012 | | bool isIntermediateStream = true; |
| | | 1013 | | |
| | 0 | 1014 | | switch (CompressionMethod) |
| | | 1015 | | { |
| | | 1016 | | case ZipCompressionMethod.Stored: |
| | 0 | 1017 | | compressorStreamFactory = () => backingStream; |
| | 0 | 1018 | | isIntermediateStream = false; |
| | 0 | 1019 | | break; |
| | | 1020 | | case ZipCompressionMethod.Deflate: |
| | | 1021 | | case ZipCompressionMethod.Deflate64: |
| | | 1022 | | default: |
| | 0 | 1023 | | compressorStreamFactory = () => new DeflateStream(backingStream, _compressionLevel, leaveBackingStre |
| | 0 | 1024 | | break; |
| | | 1025 | | } |
| | 0 | 1026 | | bool leaveCompressorStreamOpenOnClose = leaveBackingStreamOpen && !isIntermediateStream; |
| | 0 | 1027 | | var checkSumStream = new CheckSumAndSizeWriteStream( |
| | 0 | 1028 | | compressorStreamFactory, |
| | 0 | 1029 | | streamForPosition ?? backingStream, |
| | 0 | 1030 | | leaveCompressorStreamOpenOnClose, |
| | 0 | 1031 | | this, |
| | 0 | 1032 | | onClose, |
| | 0 | 1033 | | (long initialPosition, long currentPosition, uint checkSum, Stream backing, ZipArchiveEntry thisRef, Eve |
| | 0 | 1034 | | { |
| | 0 | 1035 | | thisRef._crc32 = checkSum; |
| | 0 | 1036 | | thisRef._uncompressedSize = currentPosition; |
| | 0 | 1037 | | thisRef._compressedSize = backing.Position - initialPosition; |
| | 0 | 1038 | | closeHandler?.Invoke(thisRef, EventArgs.Empty); |
| | 0 | 1039 | | }); |
| | | 1040 | | |
| | 0 | 1041 | | return checkSumStream; |
| | 0 | 1042 | | } |
| | | 1043 | | |
| | | 1044 | | private byte CalculateZipCryptoCheckByte() |
| | 0 | 1045 | | { |
| | | 1046 | | // If data descriptor NOT used, the check byte is the MSB of CRC32 |
| | 0 | 1047 | | if ((_generalPurposeBitFlag & BitFlagValues.DataDescriptor) == 0) |
| | 0 | 1048 | | { |
| | 0 | 1049 | | return (byte)((_crc32 >> 24) & 0xFF); |
| | | 1050 | | } |
| | | 1051 | | |
| | | 1052 | | // If data descriptor IS used, the check byte is the MSB of the DOS time from the *local* header |
| | 0 | 1053 | | return (byte)((ZipHelper.DateTimeToDosTime(_lastModified.DateTime) >> 8) & 0xFF); |
| | 0 | 1054 | | } |
| | | 1055 | | |
| | 0 | 1056 | | private bool IsZipCryptoEncrypted => (_generalPurposeBitFlag & BitFlagValues.IsEncrypted) != 0 && (ushort)_heade |
| | | 1057 | | |
| | 0 | 1058 | | private bool UseAesEncryption => Encryption is ZipEncryptionMethod.Aes128 or ZipEncryptionMethod.Aes192 or ZipEn |
| | | 1059 | | |
| | 1806 | 1060 | | private bool IsAesEncrypted => (ushort)_headerCompressionMethod == WinZipAesMethod; |
| | | 1061 | | |
| | | 1062 | | private static int GetAesKeySizeBits(ZipEncryptionMethod encryption) |
| | 40 | 1063 | | { |
| | | 1064 | | // Get number of bits for AES key size based on the encryption method |
| | | 1065 | | // Only possible values are: AES-128 = 128 bits, AES-192 = 192 bits, AES-256 as per the specs |
| | 40 | 1066 | | return encryption switch |
| | 40 | 1067 | | { |
| | 0 | 1068 | | ZipEncryptionMethod.Aes128 => 128, |
| | 0 | 1069 | | ZipEncryptionMethod.Aes192 => 192, |
| | 0 | 1070 | | ZipEncryptionMethod.Aes256 => 256, |
| | 40 | 1071 | | _ => throw new InvalidDataException(SR.InvalidAesStrength) |
| | 40 | 1072 | | }; |
| | 0 | 1073 | | } |
| | | 1074 | | |
| | | 1075 | | /// <summary> |
| | | 1076 | | /// Creates the appropriate decryption stream for an encrypted entry. |
| | | 1077 | | /// For AES entries, uses the salt that was pre-read during central directory parsing. |
| | | 1078 | | /// </summary> |
| | | 1079 | | private Stream WrapWithDecryptionIfNeeded(Stream compressedStream, ReadOnlySpan<char> password) |
| | 0 | 1080 | | { |
| | 0 | 1081 | | if (Encryption == ZipEncryptionMethod.Unknown) |
| | 0 | 1082 | | { |
| | 0 | 1083 | | throw new NotSupportedException(SR.UnsupportedEncryptionMethod); |
| | | 1084 | | } |
| | | 1085 | | |
| | 0 | 1086 | | if (password.IsEmpty) |
| | 0 | 1087 | | { |
| | 0 | 1088 | | throw new InvalidDataException(SR.PasswordRequired); |
| | | 1089 | | } |
| | | 1090 | | |
| | 0 | 1091 | | if (IsAesEncrypted) |
| | 0 | 1092 | | { |
| | 0 | 1093 | | if (_aesSalt is null) |
| | 0 | 1094 | | { |
| | 0 | 1095 | | throw new InvalidDataException(SR.LocalFileHeaderCorrupt); |
| | | 1096 | | } |
| | | 1097 | | |
| | 0 | 1098 | | int keySizeBits = GetAesKeySizeBits(Encryption); |
| | 0 | 1099 | | WinZipAesKeyMaterial keyMaterial = WinZipAesStream.CreateKey(password, _aesSalt, keySizeBits); |
| | 0 | 1100 | | return WinZipAesStream.Create( |
| | 0 | 1101 | | baseStream: compressedStream, |
| | 0 | 1102 | | keyMaterial: keyMaterial, |
| | 0 | 1103 | | totalStreamSize: _compressedSize, |
| | 0 | 1104 | | encrypting: false); |
| | | 1105 | | } |
| | | 1106 | | |
| | 0 | 1107 | | if (IsZipCryptoEncrypted) |
| | 0 | 1108 | | { |
| | 0 | 1109 | | byte expectedCheckByte = CalculateZipCryptoCheckByte(); |
| | 0 | 1110 | | ZipCryptoKeys keyMaterial = ZipCryptoStream.CreateKey(password); |
| | 0 | 1111 | | return ZipCryptoStream.Create(compressedStream, keyMaterial, expectedCheckByte, encrypting: false); |
| | | 1112 | | } |
| | | 1113 | | |
| | 0 | 1114 | | throw new NotSupportedException(SR.UnsupportedEncryptionMethod); |
| | 0 | 1115 | | } |
| | | 1116 | | |
| | | 1117 | | private Stream GetDataDecompressor(Stream compressedStreamToRead) |
| | 0 | 1118 | | { |
| | | 1119 | | Stream? uncompressedStream; |
| | 0 | 1120 | | switch (CompressionMethod) |
| | | 1121 | | { |
| | | 1122 | | case ZipCompressionMethod.Deflate: |
| | 0 | 1123 | | uncompressedStream = new DeflateStream(compressedStreamToRead, CompressionMode.Decompress, _uncompre |
| | 0 | 1124 | | break; |
| | | 1125 | | case ZipCompressionMethod.Deflate64: |
| | 0 | 1126 | | uncompressedStream = new DeflateManagedStream(compressedStreamToRead, ZipCompressionMethod.Deflate64 |
| | 0 | 1127 | | break; |
| | | 1128 | | case ZipCompressionMethod.Stored: |
| | 0 | 1129 | | uncompressedStream = compressedStreamToRead; |
| | 0 | 1130 | | break; |
| | | 1131 | | default: |
| | | 1132 | | // We should not get here with Aes as CompressionMethod anymore |
| | | 1133 | | // as it should have been replaced with the actual compression method |
| | 0 | 1134 | | Debug.Assert((ushort)CompressionMethod != WinZipAesMethod, |
| | 0 | 1135 | | "AES compression method should have been replaced with actual compression method"); |
| | | 1136 | | |
| | | 1137 | | // Fallback to stored if we somehow get here |
| | 0 | 1138 | | uncompressedStream = compressedStreamToRead; |
| | 0 | 1139 | | break; |
| | | 1140 | | } |
| | | 1141 | | |
| | 0 | 1142 | | return uncompressedStream; |
| | 0 | 1143 | | } |
| | | 1144 | | |
| | | 1145 | | private Stream OpenInReadMode(bool checkOpenable, ReadOnlySpan<char> password = default) |
| | 0 | 1146 | | { |
| | 0 | 1147 | | if (checkOpenable) |
| | 0 | 1148 | | { |
| | 0 | 1149 | | ThrowIfNotOpenable(needToUncompress: true, needToLoadIntoMemory: false); |
| | 0 | 1150 | | } |
| | 0 | 1151 | | return OpenInReadModeGetDataCompressor(GetOffsetOfCompressedData(), password); |
| | 0 | 1152 | | } |
| | | 1153 | | |
| | | 1154 | | private Stream OpenInReadModeGetDataCompressor(long offsetOfCompressedData, ReadOnlySpan<char> password = defaul |
| | 0 | 1155 | | { |
| | 0 | 1156 | | Stream compressedStream = new SubReadStream(_archive.ArchiveStream, offsetOfCompressedData, _compressedSize) |
| | | 1157 | | Stream streamToDecompress; |
| | | 1158 | | |
| | 0 | 1159 | | if (IsEncrypted) |
| | 0 | 1160 | | { |
| | 0 | 1161 | | streamToDecompress = WrapWithDecryptionIfNeeded(compressedStream, password); |
| | 0 | 1162 | | } |
| | | 1163 | | else |
| | 0 | 1164 | | { |
| | 0 | 1165 | | streamToDecompress = compressedStream; |
| | 0 | 1166 | | } |
| | | 1167 | | |
| | 0 | 1168 | | return BuildDecompressionPipeline(streamToDecompress); |
| | 0 | 1169 | | } |
| | | 1170 | | |
| | | 1171 | | /// <summary> |
| | | 1172 | | /// Wraps a (possibly decrypted) stream with decompression and CRC validation. |
| | | 1173 | | /// Shared by both sync and async read paths. |
| | | 1174 | | /// </summary> |
| | | 1175 | | private Stream BuildDecompressionPipeline(Stream streamToDecompress) |
| | 0 | 1176 | | { |
| | 0 | 1177 | | Stream decompressedStream = GetDataDecompressor(streamToDecompress); |
| | | 1178 | | |
| | | 1179 | | // AE-2 encrypted entries store CRC as 0 so skip CRC validation for those. |
| | | 1180 | | // AE-1 version entries store a valid CRC. |
| | 0 | 1181 | | if (IsAesEncrypted && _aeVersion == 2) |
| | 0 | 1182 | | { |
| | 0 | 1183 | | return decompressedStream; |
| | | 1184 | | } |
| | | 1185 | | |
| | 0 | 1186 | | return new CrcValidatingReadStream(decompressedStream, _crc32, _uncompressedSize); |
| | 0 | 1187 | | } |
| | | 1188 | | |
| | | 1189 | | private WrappedStream OpenInWriteMode() |
| | 0 | 1190 | | { |
| | 0 | 1191 | | if (_everOpenedForWrite) |
| | 0 | 1192 | | { |
| | 0 | 1193 | | throw new IOException(SR.CreateModeWriteOnceAndOneEntryAtATime); |
| | | 1194 | | } |
| | | 1195 | | |
| | | 1196 | | // we assume that if another entry grabbed the archive stream, that it set this entry's _everOpenedForWrite |
| | 0 | 1197 | | _archive.DebugAssertIsStillArchiveStreamOwner(this); |
| | | 1198 | | |
| | 0 | 1199 | | return OpenInWriteModeCore(); |
| | 0 | 1200 | | } |
| | | 1201 | | |
| | | 1202 | | private WrappedStream OpenInWriteModeCore() |
| | 0 | 1203 | | { |
| | 0 | 1204 | | _everOpenedForWrite = true; |
| | 0 | 1205 | | Changes |= ZipArchive.ChangeState.StoredData; |
| | | 1206 | | |
| | | 1207 | | // Use the encryption method pre-configured via PrepareEncryption (CreateEntry with password). |
| | 0 | 1208 | | ZipEncryptionMethod encryptionMethod = Encryption; |
| | | 1209 | | |
| | | 1210 | | // Build the stream stack with encryption if needed |
| | 0 | 1211 | | Stream targetStream = _archive.ArchiveStream; |
| | 0 | 1212 | | Stream? encryptionStream = null; |
| | | 1213 | | |
| | 0 | 1214 | | if (encryptionMethod == ZipEncryptionMethod.ZipCrypto) |
| | 0 | 1215 | | { |
| | 0 | 1216 | | ZipCryptoKeys keyMaterial = _derivedZipCryptoKeyMaterial |
| | 0 | 1217 | | ?? throw new InvalidOperationException(SR.EmptyPassword); |
| | | 1218 | | |
| | 0 | 1219 | | ushort verifierLow2Bytes = (ushort)ZipHelper.DateTimeToDosTime(_lastModified.DateTime); |
| | | 1220 | | |
| | 0 | 1221 | | targetStream = ZipCryptoStream.Create( |
| | 0 | 1222 | | baseStream: _archive.ArchiveStream, |
| | 0 | 1223 | | keys: keyMaterial, |
| | 0 | 1224 | | passwordVerifierLow2Bytes: verifierLow2Bytes, |
| | 0 | 1225 | | encrypting: true, |
| | 0 | 1226 | | crc32: null, |
| | 0 | 1227 | | leaveOpen: true); |
| | 0 | 1228 | | encryptionStream = targetStream; |
| | 0 | 1229 | | } |
| | 0 | 1230 | | else if (encryptionMethod is ZipEncryptionMethod.Aes256 or ZipEncryptionMethod.Aes192 or ZipEncryptionMethod |
| | 0 | 1231 | | { |
| | 0 | 1232 | | WinZipAesKeyMaterial keyMaterial = _derivedAesKeyMaterial |
| | 0 | 1233 | | ?? throw new InvalidOperationException(SR.EmptyPassword); |
| | | 1234 | | |
| | 0 | 1235 | | targetStream = WinZipAesStream.Create( |
| | 0 | 1236 | | baseStream: _archive.ArchiveStream, |
| | 0 | 1237 | | keyMaterial: keyMaterial, |
| | 0 | 1238 | | totalStreamSize: -1, |
| | 0 | 1239 | | encrypting: true, |
| | 0 | 1240 | | leaveOpen: true); |
| | 0 | 1241 | | encryptionStream = targetStream; |
| | 0 | 1242 | | } |
| | | 1243 | | |
| | 0 | 1244 | | bool isAesEncryption = encryptionMethod is ZipEncryptionMethod.Aes256 or ZipEncryptionMethod.Aes192 or ZipEn |
| | | 1245 | | |
| | 0 | 1246 | | CheckSumAndSizeWriteStream crcSizeStream = GetDataCompressor( |
| | 0 | 1247 | | targetStream, |
| | 0 | 1248 | | leaveBackingStreamOpen: !isAesEncryption, |
| | 0 | 1249 | | (object? o, EventArgs e) => |
| | 0 | 1250 | | { |
| | 0 | 1251 | | // release the archive stream |
| | 0 | 1252 | | var entry = (ZipArchiveEntry)o!; |
| | 0 | 1253 | | entry._archive.ReleaseArchiveStream(entry); |
| | 0 | 1254 | | entry._outstandingWriteStream = null; |
| | 0 | 1255 | | }, |
| | 0 | 1256 | | streamForPosition: encryptionMethod != ZipEncryptionMethod.None ? _archive.ArchiveStream : null); |
| | | 1257 | | |
| | 0 | 1258 | | _outstandingWriteStream = new DirectToArchiveWriterStream(crcSizeStream, this, encryptionMethod, encryptionS |
| | | 1259 | | |
| | 0 | 1260 | | return new WrappedStream(baseStream: _outstandingWriteStream, closeBaseStream: true); |
| | 0 | 1261 | | } |
| | | 1262 | | |
| | | 1263 | | private WrappedStream OpenInUpdateMode(bool loadExistingContent = true, ReadOnlySpan<char> password = default) |
| | 0 | 1264 | | { |
| | 0 | 1265 | | if (_currentlyOpenForWrite) |
| | 0 | 1266 | | { |
| | 0 | 1267 | | throw new IOException(SR.UpdateModeOneStream); |
| | | 1268 | | } |
| | | 1269 | | |
| | 0 | 1270 | | if (Encryption == ZipEncryptionMethod.Unknown) |
| | 0 | 1271 | | { |
| | 0 | 1272 | | throw new NotSupportedException(SR.UnsupportedEncryptionMethod); |
| | | 1273 | | } |
| | | 1274 | | |
| | | 1275 | | // Encrypted entries always require a password for re-encryption, |
| | | 1276 | | // even when discarding existing content (write-only access). |
| | 0 | 1277 | | if (IsEncrypted && password.IsEmpty) |
| | 0 | 1278 | | { |
| | 0 | 1279 | | throw new ArgumentException(SR.PasswordRequired, nameof(password)); |
| | | 1280 | | } |
| | | 1281 | | |
| | 0 | 1282 | | if (loadExistingContent) |
| | 0 | 1283 | | { |
| | 0 | 1284 | | ThrowIfNotOpenable(needToUncompress: true, needToLoadIntoMemory: true); |
| | 0 | 1285 | | } |
| | | 1286 | | |
| | 0 | 1287 | | _currentlyOpenForWrite = true; |
| | | 1288 | | |
| | | 1289 | | // Set up re-encryption key material so that the rewritten entry has valid encryption headers. |
| | 0 | 1290 | | if (IsEncrypted) |
| | 0 | 1291 | | { |
| | 0 | 1292 | | SetupEncryptionKeyMaterial(password); |
| | 0 | 1293 | | } |
| | | 1294 | | |
| | 0 | 1295 | | if (loadExistingContent) |
| | 0 | 1296 | | { |
| | 0 | 1297 | | _storedUncompressedData = GetUncompressedData(password); |
| | 0 | 1298 | | } |
| | | 1299 | | else |
| | 0 | 1300 | | { |
| | 0 | 1301 | | _storedUncompressedData?.Dispose(); |
| | 0 | 1302 | | _storedUncompressedData = new MemoryStream(); |
| | | 1303 | | // Opening with loadExistingContent: false discards existing content, which is a modification |
| | 0 | 1304 | | MarkAsModified(); |
| | 0 | 1305 | | } |
| | | 1306 | | |
| | 0 | 1307 | | _storedUncompressedData.Seek(0, SeekOrigin.Begin); |
| | | 1308 | | |
| | 0 | 1309 | | return new WrappedStream(_storedUncompressedData, this, |
| | 0 | 1310 | | onClosed: thisRef => thisRef!._currentlyOpenForWrite = false, |
| | 0 | 1311 | | notifyEntryOnWrite: true); |
| | 0 | 1312 | | } |
| | | 1313 | | |
| | | 1314 | | // Returns a read-only, seekable view over the entry's current uncompressed buffer in Update mode. |
| | | 1315 | | // It reflects modifications made earlier in the current session (and is empty for a freshly created |
| | | 1316 | | // entry), unlike OpenInReadMode which reads the original bytes directly from the archive. The stream |
| | | 1317 | | // shares the underlying buffer with the entry rather than copying it, so it is not isolated from later |
| | | 1318 | | // in-place writes; callers are expected to finish reading before reopening the entry for writing. |
| | | 1319 | | private MemoryStream OpenInUpdateModeForRead() |
| | 0 | 1320 | | { |
| | 0 | 1321 | | if (_currentlyOpenForWrite) |
| | 0 | 1322 | | throw new IOException(SR.UpdateModeOneStream); |
| | | 1323 | | |
| | 0 | 1324 | | MemoryStream uncompressedData = GetUncompressedData(); |
| | 0 | 1325 | | return new MemoryStream(uncompressedData.GetBuffer(), 0, (int)uncompressedData.Length, writable: false); |
| | 0 | 1326 | | } |
| | | 1327 | | |
| | | 1328 | | /// <summary> |
| | | 1329 | | /// Marks this entry as modified, indicating that its data has changed and needs to be rewritten. |
| | | 1330 | | /// </summary> |
| | | 1331 | | internal void MarkAsModified() |
| | 0 | 1332 | | { |
| | 0 | 1333 | | _everOpenedForWrite = true; |
| | 0 | 1334 | | Changes |= ZipArchive.ChangeState.StoredData; |
| | | 1335 | | |
| | | 1336 | | // Normalize compression method when actually modifying - Deflate64 data will be |
| | | 1337 | | // re-compressed as Deflate since we don't support writing Deflate64. |
| | 0 | 1338 | | if (CompressionMethod != ZipCompressionMethod.Stored) |
| | 0 | 1339 | | { |
| | 0 | 1340 | | CompressionMethod = ZipCompressionMethod.Deflate; |
| | 0 | 1341 | | } |
| | 0 | 1342 | | } |
| | | 1343 | | |
| | | 1344 | | /// <summary> |
| | | 1345 | | /// Sets up encryption key material for re-encryption when writing back to the archive. |
| | | 1346 | | /// </summary> |
| | | 1347 | | private void SetupEncryptionKeyMaterial(ReadOnlySpan<char> password) |
| | 0 | 1348 | | { |
| | | 1349 | | // Derive and save key material for re-encryption |
| | 0 | 1350 | | if (IsZipCryptoEncrypted) |
| | 0 | 1351 | | { |
| | 0 | 1352 | | _derivedZipCryptoKeyMaterial = ZipCryptoStream.CreateKey(password); |
| | 0 | 1353 | | Encryption = ZipEncryptionMethod.ZipCrypto; |
| | 0 | 1354 | | } |
| | 0 | 1355 | | else if (UseAesEncryption) |
| | 0 | 1356 | | { |
| | | 1357 | | // Generate new salt and derive key material for AES |
| | | 1358 | | // This ensures each write uses a fresh random salt for security |
| | 0 | 1359 | | int keySizeBits = GetAesKeySizeBits(Encryption); |
| | 0 | 1360 | | _derivedAesKeyMaterial = WinZipAesStream.CreateKey(password, salt: null, keySizeBits); |
| | | 1361 | | // Encryption is already set from constructor (parsed from central directory AES extra field) |
| | 0 | 1362 | | } |
| | 0 | 1363 | | } |
| | | 1364 | | |
| | | 1365 | | /// <summary> |
| | | 1366 | | /// Pre-derives encryption key material from a password for use when the entry is later opened for writing. |
| | | 1367 | | /// Called by <see cref="ZipArchive.CreateEntry(string, ReadOnlySpan{char}, ZipEncryptionMethod)"/>. |
| | | 1368 | | /// </summary> |
| | | 1369 | | internal void PrepareEncryption(ReadOnlySpan<char> password, ZipEncryptionMethod encryptionMethod) |
| | 0 | 1370 | | { |
| | 0 | 1371 | | if (password.IsEmpty) |
| | 0 | 1372 | | { |
| | 0 | 1373 | | throw new ArgumentException(SR.EmptyPassword, nameof(password)); |
| | | 1374 | | } |
| | | 1375 | | |
| | 0 | 1376 | | if (encryptionMethod == ZipEncryptionMethod.ZipCrypto) |
| | 0 | 1377 | | { |
| | 0 | 1378 | | Encryption = encryptionMethod; |
| | 0 | 1379 | | _derivedZipCryptoKeyMaterial = ZipCryptoStream.CreateKey(password); |
| | 0 | 1380 | | } |
| | 0 | 1381 | | else if (encryptionMethod is ZipEncryptionMethod.Aes128 or ZipEncryptionMethod.Aes192 or ZipEncryptionMethod |
| | 0 | 1382 | | { |
| | | 1383 | | |
| | 0 | 1384 | | Encryption = encryptionMethod; |
| | 0 | 1385 | | int keySizeBits = GetAesKeySizeBits(encryptionMethod); |
| | 0 | 1386 | | _derivedAesKeyMaterial = WinZipAesStream.CreateKey(password, salt: null, keySizeBits); |
| | 0 | 1387 | | } |
| | | 1388 | | else |
| | 0 | 1389 | | { |
| | | 1390 | | // Covers ZipEncryptionMethod.None, ZipEncryptionMethod.Unknown, and any undefined/out-of-range value. |
| | 0 | 1391 | | throw new ArgumentOutOfRangeException(nameof(encryptionMethod), encryptionMethod, SR.EncryptionNotSpecif |
| | | 1392 | | } |
| | 0 | 1393 | | } |
| | | 1394 | | |
| | | 1395 | | /// <summary> |
| | | 1396 | | /// Creates a WinZip AES extra field for writing to local/central directory headers. |
| | | 1397 | | /// </summary> |
| | | 1398 | | private WinZipAesExtraField CreateAesExtraField() |
| | 0 | 1399 | | { |
| | 0 | 1400 | | return new WinZipAesExtraField |
| | 0 | 1401 | | { |
| | 0 | 1402 | | VendorVersion = 2, |
| | 0 | 1403 | | AesStrength = Encryption switch |
| | 0 | 1404 | | { |
| | 0 | 1405 | | ZipEncryptionMethod.Aes128 => (byte)1, |
| | 0 | 1406 | | ZipEncryptionMethod.Aes192 => (byte)2, |
| | 0 | 1407 | | ZipEncryptionMethod.Aes256 => (byte)3, |
| | 0 | 1408 | | _ => throw new InvalidDataException(SR.InvalidAesStrength) |
| | 0 | 1409 | | }, |
| | 0 | 1410 | | CompressionMethod = _compressionLevel == CompressionLevel.NoCompression |
| | 0 | 1411 | | ? (ushort)ZipCompressionMethod.Stored |
| | 0 | 1412 | | : (ushort)ZipCompressionMethod.Deflate |
| | 0 | 1413 | | }; |
| | 0 | 1414 | | } |
| | | 1415 | | |
| | | 1416 | | private bool IsOpenable(bool needToUncompress, bool needToLoadIntoMemory, out string? message) |
| | 0 | 1417 | | { |
| | 0 | 1418 | | message = null; |
| | | 1419 | | |
| | 0 | 1420 | | if (_originallyInArchive) |
| | 0 | 1421 | | { |
| | 0 | 1422 | | if (!IsOpenableInitialVerifications(needToUncompress, out message)) |
| | 0 | 1423 | | { |
| | 0 | 1424 | | return false; |
| | | 1425 | | } |
| | | 1426 | | |
| | 0 | 1427 | | if (!IsEncrypted && !ZipLocalFileHeader.TrySkipBlock(_archive.ArchiveStream)) |
| | 0 | 1428 | | { |
| | 0 | 1429 | | message = SR.LocalFileHeaderCorrupt; |
| | 0 | 1430 | | return false; |
| | | 1431 | | } |
| | 0 | 1432 | | else if (IsEncrypted && IsAesEncrypted) |
| | 0 | 1433 | | { |
| | 0 | 1434 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); |
| | | 1435 | | // AES case - skip the local file header and validate it exists. |
| | | 1436 | | // The AES metadata (encryption strength, actual compression method) was already |
| | | 1437 | | // parsed from the central directory in the constructor |
| | 0 | 1438 | | if (!ZipLocalFileHeader.TrySkipBlock(_archive.ArchiveStream)) |
| | 0 | 1439 | | { |
| | 0 | 1440 | | message = SR.LocalFileHeaderCorrupt; |
| | 0 | 1441 | | return false; |
| | | 1442 | | } |
| | 0 | 1443 | | } |
| | | 1444 | | |
| | | 1445 | | // Pass the detected encryption method to GetOffsetOfCompressedData |
| | 0 | 1446 | | long offsetOfCompressedData = GetOffsetOfCompressedData(); |
| | | 1447 | | |
| | 0 | 1448 | | if (!IsOpenableFinalVerifications(needToLoadIntoMemory, offsetOfCompressedData, out message)) |
| | 0 | 1449 | | { |
| | 0 | 1450 | | return false; |
| | | 1451 | | } |
| | | 1452 | | |
| | 0 | 1453 | | return true; |
| | | 1454 | | } |
| | | 1455 | | |
| | 0 | 1456 | | return true; |
| | 0 | 1457 | | } |
| | | 1458 | | |
| | | 1459 | | private bool IsOpenableInitialVerifications(bool needToUncompress, out string? message) |
| | 0 | 1460 | | { |
| | 0 | 1461 | | message = null; |
| | 0 | 1462 | | if (needToUncompress) |
| | 0 | 1463 | | { |
| | | 1464 | | // For AES-encrypted entries, CompressionMethod now contains the actual compression |
| | | 1465 | | // method (from the AES extra field), not the wrapper value 99. So we can use |
| | | 1466 | | // the same validation logic for both encrypted and non-encrypted entries. |
| | 0 | 1467 | | if (CompressionMethod != ZipCompressionMethod.Stored && |
| | 0 | 1468 | | CompressionMethod != ZipCompressionMethod.Deflate && |
| | 0 | 1469 | | CompressionMethod != ZipCompressionMethod.Deflate64) |
| | 0 | 1470 | | { |
| | 0 | 1471 | | message = SR.UnsupportedCompression; |
| | 0 | 1472 | | return false; |
| | | 1473 | | } |
| | 0 | 1474 | | } |
| | 0 | 1475 | | if (_diskNumberStart != _archive.NumberOfThisDisk) |
| | 0 | 1476 | | { |
| | 0 | 1477 | | message = SR.SplitSpanned; |
| | 0 | 1478 | | return false; |
| | | 1479 | | } |
| | 0 | 1480 | | if (_offsetOfLocalHeader > _archive.ArchiveStream.Length) |
| | 0 | 1481 | | { |
| | 0 | 1482 | | message = SR.LocalFileHeaderCorrupt; |
| | 0 | 1483 | | return false; |
| | | 1484 | | } |
| | | 1485 | | |
| | 0 | 1486 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin); |
| | 0 | 1487 | | return true; |
| | 0 | 1488 | | } |
| | | 1489 | | |
| | | 1490 | | private bool IsOpenableFinalVerifications(bool needToLoadIntoMemory, long offsetOfCompressedData, out string? me |
| | 0 | 1491 | | { |
| | 0 | 1492 | | message = null; |
| | | 1493 | | // Use an overflow-safe form: corrupt zip64 archives can report compressed sizes / offsets |
| | | 1494 | | // near long.MaxValue, which would wrap around in 'offsetOfCompressedData + _compressedSize'. |
| | 0 | 1495 | | if (offsetOfCompressedData < 0 || |
| | 0 | 1496 | | _compressedSize < 0 || |
| | 0 | 1497 | | _compressedSize > _archive.ArchiveStream.Length - offsetOfCompressedData) |
| | 0 | 1498 | | { |
| | 0 | 1499 | | message = SR.LocalFileHeaderCorrupt; |
| | 0 | 1500 | | return false; |
| | | 1501 | | } |
| | | 1502 | | // This limitation originally existed because a) it is unreasonable to load > 4GB into memory |
| | | 1503 | | // but also because the stream reading functions make it hard. This has been updated to handle |
| | | 1504 | | // this scenario in a 64-bit process using multiple buffers, delivered first as an OOB for |
| | | 1505 | | // compatibility. |
| | 0 | 1506 | | if (needToLoadIntoMemory) |
| | 0 | 1507 | | { |
| | 0 | 1508 | | if (_compressedSize > int.MaxValue) |
| | 0 | 1509 | | { |
| | 0 | 1510 | | if (!s_allowLargeZipArchiveEntriesInUpdateMode) |
| | 0 | 1511 | | { |
| | 0 | 1512 | | message = SR.EntryTooLarge; |
| | 0 | 1513 | | return false; |
| | | 1514 | | } |
| | 0 | 1515 | | } |
| | 0 | 1516 | | } |
| | | 1517 | | |
| | 0 | 1518 | | return true; |
| | 0 | 1519 | | } |
| | | 1520 | | |
| | 0 | 1521 | | private bool AreSizesTooLarge => _compressedSize > uint.MaxValue || _uncompressedSize > uint.MaxValue; |
| | | 1522 | | |
| | | 1523 | | /// <summary> |
| | | 1524 | | /// The position immediately after all entry data (including any trailing data descriptor). |
| | | 1525 | | /// Computed while reading the central directory from the next entry's local header offset |
| | | 1526 | | /// or, for the last entry, the central directory start offset. |
| | | 1527 | | /// </summary> |
| | | 1528 | | internal long EndOfLocalEntryData |
| | | 1529 | | { |
| | 0 | 1530 | | get => _endOfLocalEntryData; |
| | 0 | 1531 | | set => _endOfLocalEntryData = value; |
| | | 1532 | | } |
| | | 1533 | | |
| | | 1534 | | private static CompressionLevel MapCompressionLevel(BitFlagValues generalPurposeBitFlag, ZipCompressionMethod co |
| | 29756 | 1535 | | { |
| | | 1536 | | // Information about the Deflate compression option is stored in bits 1 and 2 of the general purpose bit fla |
| | | 1537 | | // If the compression method is not Deflate, the Deflate compression option is invalid - default to NoCompre |
| | 29756 | 1538 | | if (compressionMethod == ZipCompressionMethod.Deflate || compressionMethod == ZipCompressionMethod.Deflate64 |
| | 7638 | 1539 | | { |
| | 7638 | 1540 | | return ((int)generalPurposeBitFlag & 0x6) switch |
| | 7638 | 1541 | | { |
| | 1398 | 1542 | | 0 => CompressionLevel.Optimal, |
| | 5974 | 1543 | | 2 => CompressionLevel.SmallestSize, |
| | 56 | 1544 | | 4 => CompressionLevel.Fastest, |
| | 210 | 1545 | | 6 => CompressionLevel.Fastest, |
| | 0 | 1546 | | _ => CompressionLevel.Optimal |
| | 7638 | 1547 | | }; |
| | | 1548 | | } |
| | | 1549 | | else |
| | 22118 | 1550 | | { |
| | 22118 | 1551 | | return CompressionLevel.NoCompression; |
| | | 1552 | | } |
| | 29756 | 1553 | | } |
| | | 1554 | | |
| | | 1555 | | private static BitFlagValues MapDeflateCompressionOption(BitFlagValues generalPurposeBitFlag, CompressionLevel c |
| | 0 | 1556 | | { |
| | 0 | 1557 | | ushort deflateCompressionOptions = (ushort)( |
| | 0 | 1558 | | // The Deflate compression level is only valid if the compression method is actually Deflate (or Deflate |
| | 0 | 1559 | | // value of the two bits is undefined and they should be zeroed out. |
| | 0 | 1560 | | compressionMethod == ZipCompressionMethod.Deflate || compressionMethod == ZipCompressionMethod.Deflate64 |
| | 0 | 1561 | | ? compressionLevel switch |
| | 0 | 1562 | | { |
| | 0 | 1563 | | CompressionLevel.Optimal => 0, |
| | 0 | 1564 | | CompressionLevel.SmallestSize => 2, |
| | 0 | 1565 | | CompressionLevel.Fastest => 6, |
| | 0 | 1566 | | CompressionLevel.NoCompression => 6, |
| | 0 | 1567 | | _ => 0 |
| | 0 | 1568 | | } |
| | 0 | 1569 | | : 0); |
| | | 1570 | | |
| | 0 | 1571 | | return (BitFlagValues)(((int)generalPurposeBitFlag & ~0x6) | deflateCompressionOptions); |
| | 0 | 1572 | | } |
| | | 1573 | | |
| | 0 | 1574 | | private bool IsOffsetTooLarge => _offsetOfLocalHeader > uint.MaxValue; |
| | | 1575 | | |
| | 0 | 1576 | | private bool ShouldUseZIP64 => AreSizesTooLarge || IsOffsetTooLarge; |
| | 1948 | 1577 | | internal ZipEncryptionMethod Encryption { get => _encryptionMethod; private set => _encryptionMethod = value; } |
| | | 1578 | | |
| | | 1579 | | private unsafe bool WriteLocalFileHeaderInitialize(bool isEmptyFile, bool forceWrite, bool preserveDataDescripto |
| | 0 | 1580 | | { |
| | | 1581 | | // _entryname only gets set when we read in or call moveTo. MoveTo does a check, and |
| | | 1582 | | // reading in should not be able to produce an entryname longer than ushort.MaxValue |
| | 0 | 1583 | | Debug.Assert(_storedEntryNameBytes.Length <= ushort.MaxValue); |
| | | 1584 | | |
| | | 1585 | | // decide if we need the Zip64 extra field: |
| | 0 | 1586 | | zip64ExtraField = null; |
| | | 1587 | | |
| | | 1588 | | // save offset |
| | 0 | 1589 | | _offsetOfLocalHeader = _archive.ArchiveStream.Position; |
| | | 1590 | | |
| | | 1591 | | // for extra winzip aes header |
| | 0 | 1592 | | WinZipAesExtraField? aesExtraField = null; |
| | 0 | 1593 | | int aesExtraFieldSize = 0; |
| | | 1594 | | |
| | | 1595 | | // if we already know that we have an empty file don't worry about anything, just do a straight shot of the |
| | 0 | 1596 | | if (isEmptyFile) |
| | 0 | 1597 | | { |
| | 0 | 1598 | | CompressionMethod = ZipCompressionMethod.Stored; |
| | 0 | 1599 | | compressedSizeTruncated = 0; |
| | 0 | 1600 | | uncompressedSizeTruncated = 0; |
| | 0 | 1601 | | Debug.Assert(_uncompressedSize == 0); |
| | 0 | 1602 | | Debug.Assert(_crc32 == 0); |
| | 0 | 1603 | | } |
| | | 1604 | | else |
| | 0 | 1605 | | { |
| | 0 | 1606 | | bool isCreateMode = _archive.Mode == ZipArchiveMode.Create; |
| | | 1607 | | |
| | 0 | 1608 | | if (Encryption == ZipEncryptionMethod.ZipCrypto) |
| | 0 | 1609 | | { |
| | 0 | 1610 | | _generalPurposeBitFlag |= BitFlagValues.IsEncrypted; |
| | 0 | 1611 | | _generalPurposeBitFlag |= BitFlagValues.DataDescriptor; |
| | 0 | 1612 | | compressedSizeTruncated = 0; |
| | 0 | 1613 | | uncompressedSizeTruncated = 0; |
| | 0 | 1614 | | } |
| | 0 | 1615 | | else if (UseAesEncryption) |
| | 0 | 1616 | | { |
| | 0 | 1617 | | _generalPurposeBitFlag |= BitFlagValues.IsEncrypted; |
| | 0 | 1618 | | CompressionMethod = (ZipCompressionMethod)WinZipAesMethod; |
| | 0 | 1619 | | compressedSizeTruncated = 0; |
| | 0 | 1620 | | uncompressedSizeTruncated = 0; |
| | 0 | 1621 | | aesExtraField = CreateAesExtraField(); |
| | 0 | 1622 | | aesExtraFieldSize = WinZipAesExtraField.TotalSize; |
| | 0 | 1623 | | } |
| | 0 | 1624 | | else if (isCreateMode && !_archive.ArchiveStream.CanSeek) |
| | 0 | 1625 | | { |
| | 0 | 1626 | | _generalPurposeBitFlag |= BitFlagValues.DataDescriptor; |
| | 0 | 1627 | | compressedSizeTruncated = 0; |
| | 0 | 1628 | | uncompressedSizeTruncated = 0; |
| | 0 | 1629 | | Debug.Assert(_crc32 == 0); |
| | 0 | 1630 | | } |
| | | 1631 | | else |
| | 0 | 1632 | | { |
| | | 1633 | | |
| | 0 | 1634 | | if (ShouldUseZIP64 |
| | 0 | 1635 | | #if DEBUG_FORCE_ZIP64 |
| | 0 | 1636 | | || (_archive._forceZip64 && _archive.Mode == ZipArchiveMode.Update) |
| | 0 | 1637 | | #endif |
| | 0 | 1638 | | ) |
| | 0 | 1639 | | { |
| | 0 | 1640 | | compressedSizeTruncated = ZipHelper.Mask32Bit; |
| | 0 | 1641 | | uncompressedSizeTruncated = ZipHelper.Mask32Bit; |
| | | 1642 | | |
| | 0 | 1643 | | zip64ExtraField = new() |
| | 0 | 1644 | | { |
| | 0 | 1645 | | CompressedSize = _compressedSize, |
| | 0 | 1646 | | UncompressedSize = _uncompressedSize, |
| | 0 | 1647 | | }; |
| | | 1648 | | |
| | 0 | 1649 | | VersionToExtractAtLeast(ZipVersionNeededValues.Zip64); |
| | 0 | 1650 | | } |
| | | 1651 | | else |
| | 0 | 1652 | | { |
| | 0 | 1653 | | compressedSizeTruncated = (uint)_compressedSize; |
| | 0 | 1654 | | uncompressedSizeTruncated = (uint)_uncompressedSize; |
| | 0 | 1655 | | } |
| | 0 | 1656 | | } |
| | 0 | 1657 | | } |
| | | 1658 | | |
| | | 1659 | | // save offset |
| | 0 | 1660 | | _offsetOfLocalHeader = _archive.ArchiveStream.Position; |
| | | 1661 | | |
| | | 1662 | | // Calculate extra field |
| | | 1663 | | // When using AES encryption, exclude the AES tag from currExtraFieldDataLength since we're writing a new on |
| | 0 | 1664 | | int currExtraFieldDataLength = UseAesEncryption |
| | 0 | 1665 | | ? ZipGenericExtraField.TotalSizeExcludingTag(_lhUnknownExtraFields, _lhTrailingExtraFieldData?.Length ?? |
| | 0 | 1666 | | : ZipGenericExtraField.TotalSize(_lhUnknownExtraFields, _lhTrailingExtraFieldData?.Length ?? 0); |
| | 0 | 1667 | | int bigExtraFieldLength = (zip64ExtraField != null ? zip64ExtraField.TotalSize : 0) |
| | 0 | 1668 | | + aesExtraFieldSize |
| | 0 | 1669 | | + currExtraFieldDataLength; |
| | | 1670 | | |
| | 0 | 1671 | | if (bigExtraFieldLength > ushort.MaxValue) |
| | 0 | 1672 | | { |
| | 0 | 1673 | | extraFieldLength = (ushort)((zip64ExtraField != null ? zip64ExtraField.TotalSize : 0) + aesExtraFieldSiz |
| | 0 | 1674 | | _lhUnknownExtraFields = null; |
| | 0 | 1675 | | } |
| | | 1676 | | else |
| | 0 | 1677 | | { |
| | 0 | 1678 | | extraFieldLength = (ushort)bigExtraFieldLength; |
| | 0 | 1679 | | } |
| | | 1680 | | |
| | 0 | 1681 | | crc32ToWrite = _crc32; |
| | | 1682 | | |
| | | 1683 | | // For AE-2, CRC is always 0 in the local file header |
| | 0 | 1684 | | if (UseAesEncryption) |
| | 0 | 1685 | | { |
| | 0 | 1686 | | crc32ToWrite = 0; |
| | 0 | 1687 | | } |
| | | 1688 | | |
| | | 1689 | | // If this is an existing, unchanged entry then silently skip forwards. |
| | | 1690 | | // If it's new or changed, write the header. |
| | 0 | 1691 | | if (_originallyInArchive && Changes == ZipArchive.ChangeState.Unchanged && !forceWrite) |
| | 0 | 1692 | | { |
| | 0 | 1693 | | _archive.ArchiveStream.Seek(ZipLocalFileHeader.SizeOfLocalHeader + _storedEntryNameBytes.Length + extraF |
| | | 1694 | | |
| | | 1695 | | |
| | | 1696 | | |
| | 0 | 1697 | | return false; |
| | | 1698 | | } |
| | | 1699 | | |
| | | 1700 | | // We are writing the header. For seekable/empty-file paths the sizes are written |
| | | 1701 | | // directly into the header, so a data descriptor is not needed. |
| | 0 | 1702 | | if (isEmptyFile || _archive.ArchiveStream.CanSeek) |
| | 0 | 1703 | | { |
| | 0 | 1704 | | if (preserveDataDescriptor) |
| | 0 | 1705 | | { |
| | 0 | 1706 | | compressedSizeTruncated = 0; |
| | 0 | 1707 | | uncompressedSizeTruncated = 0; |
| | | 1708 | | |
| | | 1709 | | // zero the CRC/sizes since the real values live in the trailing descriptor that remains on disk. |
| | 0 | 1710 | | crc32ToWrite = 0; |
| | | 1711 | | |
| | 0 | 1712 | | if (zip64ExtraField is not null) |
| | 0 | 1713 | | { |
| | 0 | 1714 | | zip64ExtraField = new() { CompressedSize = 0, UncompressedSize = 0 }; |
| | 0 | 1715 | | } |
| | 0 | 1716 | | } |
| | 0 | 1717 | | else if (Encryption != ZipEncryptionMethod.ZipCrypto) |
| | 0 | 1718 | | { |
| | 0 | 1719 | | _generalPurposeBitFlag &= ~BitFlagValues.DataDescriptor; |
| | 0 | 1720 | | } |
| | 0 | 1721 | | } |
| | | 1722 | | |
| | 0 | 1723 | | return true; |
| | 0 | 1724 | | } |
| | | 1725 | | |
| | | 1726 | | private void WriteLocalFileHeaderPrepare(Span<byte> lfStaticHeader, uint crc32, uint compressedSizeTruncated, ui |
| | 0 | 1727 | | { |
| | 0 | 1728 | | ZipLocalFileHeader.SignatureConstantBytes.CopyTo(lfStaticHeader[ZipLocalFileHeader.FieldLocations.Signature. |
| | 0 | 1729 | | BinaryPrimitives.WriteUInt16LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.VersionNeededToExt |
| | 0 | 1730 | | BinaryPrimitives.WriteUInt16LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.GeneralPurposeBitF |
| | | 1731 | | |
| | | 1732 | | // For AES encryption, write compression method 99 (Aes) in the header |
| | 0 | 1733 | | ushort compressionMethodToWrite = UseAesEncryption ? (ushort)WinZipAesMethod : (ushort)CompressionMethod; |
| | 0 | 1734 | | BinaryPrimitives.WriteUInt16LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.CompressionMethod. |
| | | 1735 | | |
| | 0 | 1736 | | BinaryPrimitives.WriteUInt32LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.LastModified..], Z |
| | 0 | 1737 | | BinaryPrimitives.WriteUInt32LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.Crc32..], crc32); |
| | 0 | 1738 | | BinaryPrimitives.WriteUInt32LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.CompressedSize..], |
| | 0 | 1739 | | BinaryPrimitives.WriteUInt32LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.UncompressedSize.. |
| | 0 | 1740 | | BinaryPrimitives.WriteUInt16LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.FilenameLength..], |
| | 0 | 1741 | | BinaryPrimitives.WriteUInt16LittleEndian(lfStaticHeader[ZipLocalFileHeader.FieldLocations.ExtraFieldLength.. |
| | 0 | 1742 | | } |
| | | 1743 | | |
| | | 1744 | | // return value is true if we allocated an extra field for 64 bit headers, un/compressed size |
| | | 1745 | | private unsafe bool WriteLocalFileHeader(bool isEmptyFile, bool forceWrite, bool preserveDataDescriptor = false) |
| | 0 | 1746 | | { |
| | 0 | 1747 | | if (WriteLocalFileHeaderInitialize(isEmptyFile, forceWrite, preserveDataDescriptor, out Zip64ExtraField? zip |
| | 0 | 1748 | | { |
| | 0 | 1749 | | Span<byte> lfStaticHeader = stackalloc byte[ZipLocalFileHeader.SizeOfLocalHeader]; |
| | 0 | 1750 | | WriteLocalFileHeaderPrepare(lfStaticHeader, crc32ToWrite, compressedSizeTruncated, uncompressedSizeTrunc |
| | | 1751 | | |
| | | 1752 | | // write header |
| | 0 | 1753 | | _archive.ArchiveStream.Write(lfStaticHeader); |
| | 0 | 1754 | | _archive.ArchiveStream.Write(_storedEntryNameBytes); |
| | | 1755 | | |
| | | 1756 | | // Write Zip64 extra field if needed |
| | 0 | 1757 | | zip64ExtraField?.WriteBlock(_archive.ArchiveStream); |
| | | 1758 | | |
| | | 1759 | | // Write AES extra field if using AES encryption |
| | 0 | 1760 | | if (UseAesEncryption) |
| | 0 | 1761 | | { |
| | 0 | 1762 | | CreateAesExtraField().WriteBlock(_archive.ArchiveStream); |
| | | 1763 | | |
| | | 1764 | | // Write other extra fields, excluding any existing AES extra field to avoid duplication |
| | 0 | 1765 | | ZipGenericExtraField.WriteAllBlocksExcludingTag(_lhUnknownExtraFields, _lhTrailingExtraFieldData ?? |
| | 0 | 1766 | | } |
| | | 1767 | | else |
| | 0 | 1768 | | { |
| | | 1769 | | // Write other extra fields |
| | 0 | 1770 | | ZipGenericExtraField.WriteAllBlocks(_lhUnknownExtraFields, _lhTrailingExtraFieldData ?? Array.Empty< |
| | 0 | 1771 | | } |
| | 0 | 1772 | | } |
| | | 1773 | | |
| | 0 | 1774 | | return zip64ExtraField != null; |
| | 0 | 1775 | | } |
| | | 1776 | | |
| | | 1777 | | private unsafe void WriteLocalFileHeaderAndDataIfNeeded(bool forceWrite) |
| | 0 | 1778 | | { |
| | | 1779 | | // Check if the entry's stored data was actually modified (StoredData flag is set). |
| | | 1780 | | // If _storedUncompressedData is loaded but StoredData is not set, it means the entry |
| | | 1781 | | // was opened for update but no writes occurred - we should use the original compressed bytes. |
| | 0 | 1782 | | bool storedDataModified = (Changes & ZipArchive.ChangeState.StoredData) != 0; |
| | | 1783 | | |
| | | 1784 | | // If _storedUncompressedData is loaded but not modified, clear it so we use _compressedBytes |
| | 0 | 1785 | | if (_storedUncompressedData != null && !storedDataModified) |
| | 0 | 1786 | | { |
| | 0 | 1787 | | _storedUncompressedData.Dispose(); |
| | 0 | 1788 | | _storedUncompressedData = null; |
| | 0 | 1789 | | } |
| | | 1790 | | |
| | | 1791 | | // _storedUncompressedData gets frozen here, and is what gets written to the file |
| | 0 | 1792 | | if (_storedUncompressedData != null || _compressedBytes != null) |
| | 0 | 1793 | | { |
| | 0 | 1794 | | if (_storedUncompressedData != null) |
| | 0 | 1795 | | { |
| | 0 | 1796 | | _uncompressedSize = _storedUncompressedData.Length; |
| | | 1797 | | |
| | | 1798 | | // Check if we need to re-encrypt with ZipCrypto (only if we have cached key material) |
| | 0 | 1799 | | if (Encryption == ZipEncryptionMethod.ZipCrypto && _derivedZipCryptoKeyMaterial is not null) |
| | 0 | 1800 | | { |
| | 0 | 1801 | | WriteLocalFileHeader(isEmptyFile: false, forceWrite: true); |
| | | 1802 | | |
| | 0 | 1803 | | long startPosition = _archive.ArchiveStream.Position; |
| | | 1804 | | |
| | 0 | 1805 | | ushort verifierLow2Bytes = (ushort)ZipHelper.DateTimeToDosTime(_lastModified.DateTime); |
| | | 1806 | | |
| | 0 | 1807 | | using (ZipCryptoStream encryptionStream = ZipCryptoStream.Create( |
| | 0 | 1808 | | baseStream: _archive.ArchiveStream, |
| | 0 | 1809 | | keys: _derivedZipCryptoKeyMaterial.Value, |
| | 0 | 1810 | | passwordVerifierLow2Bytes: verifierLow2Bytes, |
| | 0 | 1811 | | encrypting: true, |
| | 0 | 1812 | | crc32: null, |
| | 0 | 1813 | | leaveOpen: true)) |
| | 0 | 1814 | | { |
| | 0 | 1815 | | using (CheckSumAndSizeWriteStream crcStream = GetDataCompressor(encryptionStream, leaveBacki |
| | 0 | 1816 | | { |
| | 0 | 1817 | | _storedUncompressedData.Seek(0, SeekOrigin.Begin); |
| | 0 | 1818 | | _storedUncompressedData.CopyTo(crcStream); |
| | 0 | 1819 | | } |
| | 0 | 1820 | | } |
| | | 1821 | | |
| | 0 | 1822 | | _compressedSize = _archive.ArchiveStream.Position - startPosition; |
| | | 1823 | | |
| | 0 | 1824 | | WriteDataDescriptor(); |
| | | 1825 | | |
| | 0 | 1826 | | _storedUncompressedData.Dispose(); |
| | 0 | 1827 | | _storedUncompressedData = null; |
| | 0 | 1828 | | } |
| | 0 | 1829 | | else if (UseAesEncryption && _derivedAesKeyMaterial is not null) |
| | 0 | 1830 | | { |
| | | 1831 | | |
| | 0 | 1832 | | bool usedZip64InLH = WriteLocalFileHeader(isEmptyFile: false, forceWrite: true); |
| | | 1833 | | |
| | 0 | 1834 | | long startPosition = _archive.ArchiveStream.Position; |
| | | 1835 | | |
| | 0 | 1836 | | bool useDeflate = _compressionLevel != CompressionLevel.NoCompression; |
| | | 1837 | | |
| | 0 | 1838 | | using (WinZipAesStream encryptionStream = WinZipAesStream.Create( |
| | 0 | 1839 | | baseStream: _archive.ArchiveStream, |
| | 0 | 1840 | | keyMaterial: _derivedAesKeyMaterial.Value, |
| | 0 | 1841 | | totalStreamSize: -1, |
| | 0 | 1842 | | encrypting: true, |
| | 0 | 1843 | | leaveOpen: true)) |
| | 0 | 1844 | | { |
| | 0 | 1845 | | if (_storedUncompressedData.Length > 0) |
| | 0 | 1846 | | { |
| | 0 | 1847 | | ZipCompressionMethod savedMethod = CompressionMethod; |
| | 0 | 1848 | | CompressionMethod = useDeflate ? ZipCompressionMethod.Deflate : ZipCompressionMethod.Sto |
| | | 1849 | | |
| | | 1850 | | try |
| | 0 | 1851 | | { |
| | 0 | 1852 | | using (CheckSumAndSizeWriteStream crcStream = GetDataCompressor(encryptionStream, le |
| | 0 | 1853 | | { |
| | 0 | 1854 | | _storedUncompressedData.Seek(0, SeekOrigin.Begin); |
| | 0 | 1855 | | _storedUncompressedData.CopyTo(crcStream); |
| | 0 | 1856 | | } |
| | 0 | 1857 | | } |
| | | 1858 | | finally |
| | 0 | 1859 | | { |
| | 0 | 1860 | | CompressionMethod = (ZipCompressionMethod)WinZipAesMethod; |
| | 0 | 1861 | | } |
| | 0 | 1862 | | } |
| | | 1863 | | else |
| | 0 | 1864 | | { |
| | 0 | 1865 | | _crc32 = 0; |
| | 0 | 1866 | | _uncompressedSize = 0; |
| | 0 | 1867 | | } |
| | 0 | 1868 | | } |
| | | 1869 | | |
| | 0 | 1870 | | _compressedSize = _archive.ArchiveStream.Position - startPosition; |
| | | 1871 | | |
| | 0 | 1872 | | WriteCrcAndSizesInLocalHeader(usedZip64InLH); |
| | | 1873 | | |
| | 0 | 1874 | | _storedUncompressedData.Dispose(); |
| | 0 | 1875 | | _storedUncompressedData = null; |
| | 0 | 1876 | | } |
| | | 1877 | | else |
| | 0 | 1878 | | { |
| | | 1879 | | // Non-encrypted: use standard path |
| | 0 | 1880 | | using (DirectToArchiveWriterStream entryWriter = new(GetDataCompressor(_archive.ArchiveStream, t |
| | 0 | 1881 | | { |
| | 0 | 1882 | | _storedUncompressedData.Seek(0, SeekOrigin.Begin); |
| | 0 | 1883 | | _storedUncompressedData.CopyTo(entryWriter); |
| | 0 | 1884 | | } |
| | 0 | 1885 | | _storedUncompressedData.Dispose(); |
| | 0 | 1886 | | _storedUncompressedData = null; |
| | 0 | 1887 | | } |
| | 0 | 1888 | | } |
| | | 1889 | | else // _compressedBytes path - copying unchanged entry data |
| | 0 | 1890 | | { |
| | 0 | 1891 | | if (_uncompressedSize == 0) |
| | 0 | 1892 | | { |
| | | 1893 | | // reset size to ensure proper central directory size header |
| | 0 | 1894 | | _compressedSize = 0; |
| | 0 | 1895 | | } |
| | | 1896 | | |
| | | 1897 | | // For unchanged entries, we need to write the header correctly but avoid |
| | | 1898 | | // WriteLocalFileHeader creating NEW encryption structures (which would have |
| | | 1899 | | // wrong compression method from _compressionLevel). |
| | | 1900 | | // The original AES extra field is preserved in _lhUnknownExtraFields. |
| | 0 | 1901 | | BitFlagValues savedFlags = _generalPurposeBitFlag; |
| | 0 | 1902 | | ZipEncryptionMethod savedEncryption = Encryption; |
| | 0 | 1903 | | ZipCompressionMethod savedCompressionMethod = CompressionMethod; |
| | | 1904 | | |
| | | 1905 | | try |
| | 0 | 1906 | | { |
| | | 1907 | | // For AES entries: set CompressionMethod to Aes so header writes method 99, |
| | | 1908 | | // but clear _encryptionMethod so WriteLocalFileHeader doesn't create a new |
| | | 1909 | | // AES extra field (the original one in _lhUnknownExtraFields will be used). |
| | 0 | 1910 | | if (savedEncryption is ZipEncryptionMethod.Aes128 or ZipEncryptionMethod.Aes192 or ZipEncryption |
| | 0 | 1911 | | { |
| | 0 | 1912 | | CompressionMethod = (ZipCompressionMethod)WinZipAesMethod; |
| | 0 | 1913 | | Encryption = ZipEncryptionMethod.None; |
| | 0 | 1914 | | } |
| | | 1915 | | |
| | 0 | 1916 | | WriteLocalFileHeader(isEmptyFile: _uncompressedSize == 0, forceWrite: true); |
| | | 1917 | | |
| | | 1918 | | // WriteLocalFileHeaderInitialize may have cleared the DataDescriptor flag |
| | | 1919 | | // (because Encryption was temporarily set to None and the stream is seekable). |
| | | 1920 | | // If the original entry had a data descriptor, patch the general-purpose bit |
| | | 1921 | | // flags in the already-written local header to match, so the header on disk |
| | | 1922 | | // is consistent with the data descriptor we conditionally write below. |
| | 0 | 1923 | | if ((savedFlags & BitFlagValues.DataDescriptor) != 0 && |
| | 0 | 1924 | | (_generalPurposeBitFlag & BitFlagValues.DataDescriptor) == 0) |
| | 0 | 1925 | | { |
| | 0 | 1926 | | long currentPos = _archive.ArchiveStream.Position; |
| | 0 | 1927 | | _archive.ArchiveStream.Seek( |
| | 0 | 1928 | | _offsetOfLocalHeader + ZipLocalFileHeader.FieldLocations.GeneralPurposeBitFlags, |
| | 0 | 1929 | | SeekOrigin.Begin); |
| | 0 | 1930 | | Span<byte> flagBytes = stackalloc byte[2]; |
| | 0 | 1931 | | BinaryPrimitives.WriteUInt16LittleEndian(flagBytes, (ushort)savedFlags); |
| | 0 | 1932 | | _archive.ArchiveStream.Write(flagBytes); |
| | 0 | 1933 | | _archive.ArchiveStream.Seek(currentPos, SeekOrigin.Begin); |
| | 0 | 1934 | | } |
| | 0 | 1935 | | } |
| | | 1936 | | finally |
| | 0 | 1937 | | { |
| | | 1938 | | // Restore original state |
| | 0 | 1939 | | _generalPurposeBitFlag = savedFlags; |
| | 0 | 1940 | | Encryption = savedEncryption; |
| | 0 | 1941 | | CompressionMethod = savedCompressionMethod; |
| | 0 | 1942 | | } |
| | | 1943 | | |
| | | 1944 | | // according to ZIP specs, zero-byte files MUST NOT include file data |
| | 0 | 1945 | | if (_uncompressedSize != 0) |
| | 0 | 1946 | | { |
| | 0 | 1947 | | Debug.Assert(_compressedBytes != null); |
| | 0 | 1948 | | foreach (byte[] compressedBytes in _compressedBytes) |
| | 0 | 1949 | | { |
| | 0 | 1950 | | _archive.ArchiveStream.Write(compressedBytes, 0, compressedBytes.Length); |
| | 0 | 1951 | | } |
| | 0 | 1952 | | } |
| | | 1953 | | |
| | | 1954 | | // Write data descriptor if the original entry had one |
| | 0 | 1955 | | if ((savedFlags & BitFlagValues.DataDescriptor) != 0) |
| | 0 | 1956 | | { |
| | 0 | 1957 | | WriteDataDescriptor(); |
| | 0 | 1958 | | } |
| | 0 | 1959 | | } |
| | 0 | 1960 | | } |
| | | 1961 | | else // there is no data in the file (or the data in the file has not been loaded), but if we are in update |
| | 0 | 1962 | | { |
| | 0 | 1963 | | if (_archive.Mode == ZipArchiveMode.Update || !_everOpenedForWrite) |
| | 0 | 1964 | | { |
| | 0 | 1965 | | _everOpenedForWrite = true; |
| | | 1966 | | // Preserve the data descriptor flag for entries that originally had one, |
| | | 1967 | | // since the descriptor bytes remain on disk after the compressed data. |
| | 0 | 1968 | | bool preserveDataDescriptor = _originallyInArchive |
| | 0 | 1969 | | && (_generalPurposeBitFlag & BitFlagValues.DataDescriptor) != 0; |
| | 0 | 1970 | | WriteLocalFileHeader(isEmptyFile: _uncompressedSize == 0, forceWrite: forceWrite, preserveDataDescri |
| | | 1971 | | |
| | | 1972 | | // Advance the stream past the compressed data and any trailing data descriptor |
| | | 1973 | | // by seeking to the pre-computed end-of-entry boundary. |
| | 0 | 1974 | | if (_endOfLocalEntryData > _archive.ArchiveStream.Position) |
| | 0 | 1975 | | { |
| | 0 | 1976 | | _archive.ArchiveStream.Seek(_endOfLocalEntryData, SeekOrigin.Begin); |
| | 0 | 1977 | | } |
| | 0 | 1978 | | } |
| | 0 | 1979 | | } |
| | 0 | 1980 | | } |
| | | 1981 | | |
| | | 1982 | | private const int MetadataBufferLength = ZipLocalFileHeader.FieldLengths.VersionNeededToExtract + ZipLocalFileHe |
| | | 1983 | | private const int CrcAndSizesBufferLength = ZipLocalFileHeader.FieldLengths.Crc32 + ZipLocalFileHeader.FieldLeng |
| | | 1984 | | private const int Zip64SizesBufferLength = Zip64ExtraField.FieldLengths.UncompressedSize + Zip64ExtraField.Field |
| | | 1985 | | private const int Zip64DataDescriptorCrcAndSizesBufferLength = ZipLocalFileHeader.Zip64DataDescriptor.FieldLengt |
| | | 1986 | | + ZipLocalFileHeader.Zip64DataDescriptor.FieldLengths.CompressedSize + ZipLocalFileHeader.Zip64DataDescripto |
| | | 1987 | | |
| | | 1988 | | // Using _offsetOfLocalHeader, seeks back to where CRC and sizes should be in the header, |
| | | 1989 | | // writes them, then seeks back to where you started |
| | | 1990 | | // Assumes that the stream is currently at the end of the data |
| | | 1991 | | private unsafe void WriteCrcAndSizesInLocalHeader(bool zip64HeaderUsed) |
| | 0 | 1992 | | { |
| | | 1993 | | // Buffer has been sized to the largest data payload required: the 64-bit data descriptor. |
| | 0 | 1994 | | Span<byte> writeBuffer = stackalloc byte[Zip64DataDescriptorCrcAndSizesBufferLength]; |
| | | 1995 | | |
| | 0 | 1996 | | WriteCrcAndSizesInLocalHeaderInitialize(zip64HeaderUsed, out long finalPosition, out bool pretendStreaming, |
| | | 1997 | | |
| | | 1998 | | // first step is, if we need zip64, but didn't allocate it, pretend we did a stream write, because |
| | | 1999 | | // we can't go back and give ourselves the space that the extra field needs. |
| | | 2000 | | // we do this by setting the correct property in the bit flag to indicate we have a data descriptor |
| | | 2001 | | // and setting the version to Zip64 to indicate that descriptor contains 64-bit values |
| | 0 | 2002 | | if (pretendStreaming) |
| | 0 | 2003 | | { |
| | 0 | 2004 | | WriteCrcAndSizesInLocalHeaderPrepareForZip64PretendStreaming(writeBuffer); |
| | 0 | 2005 | | _archive.ArchiveStream.Write(writeBuffer[..MetadataBufferLength]); |
| | 0 | 2006 | | } |
| | | 2007 | | |
| | | 2008 | | // next step is fill out the 32-bit size values in the normal header. we can't assume that |
| | | 2009 | | // they are correct. we also write the CRC |
| | 0 | 2010 | | WriteCrcAndSizesInLocalHeaderPrepareFor32bitValuesWriting(pretendStreaming, writeBuffer, compressedSizeTrunc |
| | 0 | 2011 | | _archive.ArchiveStream.Write(writeBuffer[..CrcAndSizesBufferLength]); |
| | | 2012 | | |
| | | 2013 | | // next step: if we wrote the 64 bit header initially, a different implementation might |
| | | 2014 | | // try to read it, even if the 32-bit size values aren't masked. thus, we should always put the |
| | | 2015 | | // correct size information in there. note that order of uncomp/comp is switched, and these are |
| | | 2016 | | // 64-bit values |
| | | 2017 | | // also, note that in order for this to be correct, we have to ensure that the zip64 extra field |
| | | 2018 | | // is always the first extra field that is written |
| | 0 | 2019 | | if (zip64HeaderUsed) |
| | 0 | 2020 | | { |
| | 0 | 2021 | | WriteCrcAndSizesInLocalHeaderPrepareForWritingWhenZip64HeaderUsed(writeBuffer); |
| | 0 | 2022 | | _archive.ArchiveStream.Write(writeBuffer[..Zip64SizesBufferLength]); |
| | 0 | 2023 | | } |
| | | 2024 | | |
| | | 2025 | | // now go to the where we were. assume that this is the end of the data |
| | 0 | 2026 | | _archive.ArchiveStream.Seek(finalPosition, SeekOrigin.Begin); |
| | | 2027 | | |
| | | 2028 | | // if we are pretending we did a stream write, we want to write the data descriptor out |
| | | 2029 | | // the data descriptor can have 32-bit sizes or 64-bit sizes. In this case, we always use |
| | | 2030 | | // 64-bit sizes |
| | 0 | 2031 | | if (pretendStreaming) |
| | 0 | 2032 | | { |
| | 0 | 2033 | | WriteCrcAndSizesInLocalHeaderPrepareForWritingDataDescriptor(writeBuffer); |
| | 0 | 2034 | | _archive.ArchiveStream.Write(writeBuffer[..Zip64DataDescriptorCrcAndSizesBufferLength]); |
| | 0 | 2035 | | } |
| | 0 | 2036 | | } |
| | | 2037 | | |
| | | 2038 | | private void WriteCrcAndSizesInLocalHeaderInitialize(bool zip64HeaderUsed, out long finalPosition, out bool pret |
| | 0 | 2039 | | { |
| | 0 | 2040 | | finalPosition = _archive.ArchiveStream.Position; |
| | | 2041 | | |
| | 0 | 2042 | | bool zip64Needed = ShouldUseZIP64 |
| | 0 | 2043 | | #if DEBUG_FORCE_ZIP64 |
| | 0 | 2044 | | || _archive._forceZip64 |
| | 0 | 2045 | | #endif |
| | 0 | 2046 | | ; |
| | | 2047 | | |
| | 0 | 2048 | | pretendStreaming = zip64Needed && !zip64HeaderUsed; |
| | 0 | 2049 | | compressedSizeTruncated = zip64Needed ? ZipHelper.Mask32Bit : (uint)_compressedSize; |
| | 0 | 2050 | | uncompressedSizeTruncated = zip64Needed ? ZipHelper.Mask32Bit : (uint)_uncompressedSize; |
| | 0 | 2051 | | } |
| | | 2052 | | |
| | | 2053 | | private void WriteCrcAndSizesInLocalHeaderPrepareForZip64PretendStreaming(Span<byte> writeBuffer) |
| | 0 | 2054 | | { |
| | 0 | 2055 | | int relativeVersionToExtractLocation = ZipLocalFileHeader.FieldLocations.VersionNeededToExtract - ZipLocalFi |
| | 0 | 2056 | | int relativeGeneralPurposeBitFlagsLocation = ZipLocalFileHeader.FieldLocations.GeneralPurposeBitFlags - ZipL |
| | | 2057 | | |
| | 0 | 2058 | | VersionToExtractAtLeast(ZipVersionNeededValues.Zip64); |
| | 0 | 2059 | | _generalPurposeBitFlag |= BitFlagValues.DataDescriptor; |
| | | 2060 | | |
| | 0 | 2061 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.FieldLocations.VersionNeededToExtract, |
| | 0 | 2062 | | SeekOrigin.Begin); |
| | 0 | 2063 | | BinaryPrimitives.WriteUInt16LittleEndian(writeBuffer[relativeVersionToExtractLocation..], (ushort)_versionTo |
| | 0 | 2064 | | BinaryPrimitives.WriteUInt16LittleEndian(writeBuffer[relativeGeneralPurposeBitFlagsLocation..], (ushort)_gen |
| | 0 | 2065 | | } |
| | | 2066 | | |
| | | 2067 | | private void WriteCrcAndSizesInLocalHeaderPrepareFor32bitValuesWriting(bool pretendStreaming, Span<byte> writeBu |
| | 0 | 2068 | | { |
| | 0 | 2069 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.FieldLocations.Crc32, |
| | 0 | 2070 | | SeekOrigin.Begin); |
| | 0 | 2071 | | if (!pretendStreaming) |
| | 0 | 2072 | | { |
| | 0 | 2073 | | int relativeCrc32Location = ZipLocalFileHeader.FieldLocations.Crc32 - ZipLocalFileHeader.FieldLocations. |
| | 0 | 2074 | | int relativeCompressedSizeLocation = ZipLocalFileHeader.FieldLocations.CompressedSize - ZipLocalFileHead |
| | 0 | 2075 | | int relativeUncompressedSizeLocation = ZipLocalFileHeader.FieldLocations.UncompressedSize - ZipLocalFile |
| | | 2076 | | // when using aes encryption, ae-2 standard dictates crc to be 0 |
| | 0 | 2077 | | uint crcToWrite = UseAesEncryption ? 0 : _crc32; |
| | 0 | 2078 | | BinaryPrimitives.WriteUInt32LittleEndian(writeBuffer[relativeCrc32Location..], crcToWrite); |
| | 0 | 2079 | | BinaryPrimitives.WriteUInt32LittleEndian(writeBuffer[relativeCompressedSizeLocation..], compressedSizeTr |
| | 0 | 2080 | | BinaryPrimitives.WriteUInt32LittleEndian(writeBuffer[relativeUncompressedSizeLocation..], uncompressedSi |
| | 0 | 2081 | | } |
| | | 2082 | | else // but if we are pretending to stream, we want to fill in with zeroes |
| | 0 | 2083 | | { |
| | 0 | 2084 | | writeBuffer[..CrcAndSizesBufferLength].Clear(); |
| | 0 | 2085 | | } |
| | 0 | 2086 | | } |
| | | 2087 | | |
| | | 2088 | | private void WriteCrcAndSizesInLocalHeaderPrepareForWritingWhenZip64HeaderUsed(Span<byte> writeBuffer) |
| | 0 | 2089 | | { |
| | 0 | 2090 | | int relativeUncompressedSizeLocation = Zip64ExtraField.FieldLocations.UncompressedSize - Zip64ExtraField.Fie |
| | 0 | 2091 | | int relativeCompressedSizeLocation = Zip64ExtraField.FieldLocations.CompressedSize - Zip64ExtraField.FieldLo |
| | | 2092 | | |
| | 0 | 2093 | | _archive.ArchiveStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.SizeOfLocalHeader |
| | 0 | 2094 | | + _storedEntryNameBytes.Length + Zip64ExtraField.OffsetToFirstField, |
| | 0 | 2095 | | SeekOrigin.Begin); |
| | 0 | 2096 | | BinaryPrimitives.WriteInt64LittleEndian(writeBuffer[relativeUncompressedSizeLocation..], _uncompressedSize); |
| | 0 | 2097 | | BinaryPrimitives.WriteInt64LittleEndian(writeBuffer[relativeCompressedSizeLocation..], _compressedSize); |
| | 0 | 2098 | | } |
| | | 2099 | | |
| | | 2100 | | private void WriteCrcAndSizesInLocalHeaderPrepareForWritingDataDescriptor(Span<byte> writeBuffer) |
| | 0 | 2101 | | { |
| | 0 | 2102 | | int relativeCrc32Location = ZipLocalFileHeader.Zip64DataDescriptor.FieldLocations.Crc32 - ZipLocalFileHeader |
| | 0 | 2103 | | int relativeCompressedSizeLocation = ZipLocalFileHeader.Zip64DataDescriptor.FieldLocations.CompressedSize - |
| | 0 | 2104 | | int relativeUncompressedSizeLocation = ZipLocalFileHeader.Zip64DataDescriptor.FieldLocations.UncompressedSiz |
| | | 2105 | | // when using aes encryption, ae-2 standard dictates crc to be 0 |
| | 0 | 2106 | | uint crcToWrite = UseAesEncryption ? 0 : _crc32; |
| | 0 | 2107 | | BinaryPrimitives.WriteUInt32LittleEndian(writeBuffer.Slice(relativeCrc32Location), crcToWrite); |
| | 0 | 2108 | | BinaryPrimitives.WriteInt64LittleEndian(writeBuffer.Slice(relativeCompressedSizeLocation), _compressedSize); |
| | 0 | 2109 | | BinaryPrimitives.WriteInt64LittleEndian(writeBuffer.Slice(relativeUncompressedSizeLocation), _uncompressedSi |
| | | 2110 | | |
| | 0 | 2111 | | } |
| | | 2112 | | |
| | | 2113 | | // data descriptor can be 32-bit or 64-bit sizes. 32-bit is more compatible, so use that if possible |
| | | 2114 | | // signature is optional but recommended by the spec |
| | | 2115 | | private const int MaxSizeOfDataDescriptor = 24; |
| | | 2116 | | |
| | | 2117 | | private unsafe void WriteDataDescriptor() |
| | 0 | 2118 | | { |
| | 0 | 2119 | | Span<byte> dataDescriptor = stackalloc byte[MaxSizeOfDataDescriptor]; |
| | 0 | 2120 | | int bytesToWrite = PrepareToWriteDataDescriptor(dataDescriptor); |
| | 0 | 2121 | | _archive.ArchiveStream.Write(dataDescriptor[..bytesToWrite]); |
| | 0 | 2122 | | } |
| | | 2123 | | |
| | | 2124 | | private int PrepareToWriteDataDescriptor(Span<byte> dataDescriptor) |
| | 0 | 2125 | | { |
| | | 2126 | | // We enter here because we cannot seek, so the data descriptor bit should be on |
| | 0 | 2127 | | Debug.Assert((_generalPurposeBitFlag & BitFlagValues.DataDescriptor) != 0); |
| | | 2128 | | |
| | | 2129 | | int bytesToWrite; |
| | | 2130 | | |
| | 0 | 2131 | | ZipLocalFileHeader.DataDescriptorSignatureConstantBytes.CopyTo(dataDescriptor[ZipLocalFileHeader.ZipDataDesc |
| | | 2132 | | // when using aes encryption, ae-2 standard dictates crc to be 0 |
| | 0 | 2133 | | uint crcToWrite = UseAesEncryption ? 0 : _crc32; |
| | 0 | 2134 | | BinaryPrimitives.WriteUInt32LittleEndian(dataDescriptor[ZipLocalFileHeader.ZipDataDescriptor.FieldLocations. |
| | | 2135 | | |
| | 0 | 2136 | | if (AreSizesTooLarge) |
| | 0 | 2137 | | { |
| | 0 | 2138 | | BinaryPrimitives.WriteInt64LittleEndian(dataDescriptor[ZipLocalFileHeader.Zip64DataDescriptor.FieldLocat |
| | 0 | 2139 | | BinaryPrimitives.WriteInt64LittleEndian(dataDescriptor[ZipLocalFileHeader.Zip64DataDescriptor.FieldLocat |
| | | 2140 | | |
| | 0 | 2141 | | bytesToWrite = ZipLocalFileHeader.Zip64DataDescriptor.FieldLocations.UncompressedSize + ZipLocalFileHead |
| | 0 | 2142 | | } |
| | | 2143 | | else |
| | 0 | 2144 | | { |
| | 0 | 2145 | | BinaryPrimitives.WriteUInt32LittleEndian(dataDescriptor[ZipLocalFileHeader.ZipDataDescriptor.FieldLocati |
| | 0 | 2146 | | BinaryPrimitives.WriteUInt32LittleEndian(dataDescriptor[ZipLocalFileHeader.ZipDataDescriptor.FieldLocati |
| | | 2147 | | |
| | 0 | 2148 | | bytesToWrite = ZipLocalFileHeader.ZipDataDescriptor.FieldLocations.UncompressedSize + ZipLocalFileHeader |
| | 0 | 2149 | | } |
| | | 2150 | | |
| | 0 | 2151 | | return bytesToWrite; |
| | 0 | 2152 | | } |
| | | 2153 | | |
| | | 2154 | | internal void UnloadStreams() |
| | 0 | 2155 | | { |
| | 0 | 2156 | | _storedUncompressedData?.Dispose(); |
| | 0 | 2157 | | _compressedBytes = null; |
| | 0 | 2158 | | _outstandingWriteStream = null; |
| | 0 | 2159 | | } |
| | | 2160 | | |
| | | 2161 | | private void CloseStreams() |
| | 0 | 2162 | | { |
| | | 2163 | | // if the user left the stream open, close the underlying stream for them |
| | 0 | 2164 | | _outstandingWriteStream?.Dispose(); |
| | 0 | 2165 | | } |
| | | 2166 | | |
| | | 2167 | | private void VersionToExtractAtLeast(ZipVersionNeededValues value) |
| | 36620 | 2168 | | { |
| | 36620 | 2169 | | if (_versionToExtract < value) |
| | 7408 | 2170 | | { |
| | 7408 | 2171 | | _versionToExtract = value; |
| | 7408 | 2172 | | Changes |= ZipArchive.ChangeState.FixedLengthMetadata; |
| | 7408 | 2173 | | } |
| | 36620 | 2174 | | if (_versionMadeBySpecification < value) |
| | 27316 | 2175 | | { |
| | 27316 | 2176 | | _versionMadeBySpecification = value; |
| | 27316 | 2177 | | Changes |= ZipArchive.ChangeState.FixedLengthMetadata; |
| | 27316 | 2178 | | } |
| | 36620 | 2179 | | } |
| | | 2180 | | |
| | | 2181 | | private void ThrowIfInvalidArchive() |
| | 0 | 2182 | | { |
| | 0 | 2183 | | if (_archive == null) |
| | 0 | 2184 | | { |
| | 0 | 2185 | | throw new InvalidOperationException(SR.DeletedEntry); |
| | | 2186 | | } |
| | 0 | 2187 | | _archive.ThrowIfDisposed(); |
| | 0 | 2188 | | } |
| | | 2189 | | |
| | | 2190 | | /// <summary> |
| | | 2191 | | /// Gets the file name of the path based on Windows path separator characters |
| | | 2192 | | /// </summary> |
| | | 2193 | | private static string GetFileName_Windows(string path) |
| | 29602 | 2194 | | { |
| | 29602 | 2195 | | int i = path.AsSpan().LastIndexOfAny('\\', '/', ':'); |
| | 29602 | 2196 | | return i >= 0 ? |
| | 29602 | 2197 | | path.Substring(i + 1) : |
| | 29602 | 2198 | | path; |
| | 29602 | 2199 | | } |
| | | 2200 | | |
| | | 2201 | | /// <summary> |
| | | 2202 | | /// Gets the file name of the path based on Unix path separator characters |
| | | 2203 | | /// </summary> |
| | | 2204 | | private static string GetFileName_Unix(string path) |
| | 154 | 2205 | | { |
| | 154 | 2206 | | int i = path.LastIndexOf('/'); |
| | 154 | 2207 | | return i >= 0 ? |
| | 154 | 2208 | | path.Substring(i + 1) : |
| | 154 | 2209 | | path; |
| | 154 | 2210 | | } |
| | | 2211 | | |
| | | 2212 | | private sealed class DirectToArchiveWriterStream : Stream |
| | | 2213 | | { |
| | | 2214 | | private long _position; |
| | | 2215 | | private readonly CheckSumAndSizeWriteStream _crcSizeStream; |
| | | 2216 | | private bool _everWritten; |
| | | 2217 | | private bool _isDisposed; |
| | | 2218 | | private readonly ZipArchiveEntry _entry; |
| | | 2219 | | private bool _usedZip64inLH; |
| | | 2220 | | private bool _canWrite; |
| | | 2221 | | private readonly ZipEncryptionMethod _encryption; |
| | | 2222 | | private readonly Stream? _encryptionStream; |
| | | 2223 | | |
| | | 2224 | | // makes the assumption that somewhere down the line, crcSizeStream is eventually writing directly to the ar |
| | | 2225 | | // this class calls other functions on ZipArchiveEntry that write directly to the archive |
| | 0 | 2226 | | public DirectToArchiveWriterStream(CheckSumAndSizeWriteStream crcSizeStream, ZipArchiveEntry entry, ZipEncry |
| | 0 | 2227 | | { |
| | 0 | 2228 | | _position = 0; |
| | 0 | 2229 | | _crcSizeStream = crcSizeStream; |
| | 0 | 2230 | | _everWritten = false; |
| | 0 | 2231 | | _isDisposed = false; |
| | 0 | 2232 | | _entry = entry; |
| | 0 | 2233 | | _usedZip64inLH = false; |
| | 0 | 2234 | | _canWrite = true; |
| | 0 | 2235 | | _encryption = encryptionMethod; |
| | 0 | 2236 | | _encryptionStream = encryptionStream; |
| | 0 | 2237 | | } |
| | | 2238 | | |
| | | 2239 | | public override long Length |
| | | 2240 | | { |
| | | 2241 | | get |
| | 0 | 2242 | | { |
| | 0 | 2243 | | ThrowIfDisposed(); |
| | 0 | 2244 | | throw new NotSupportedException(SR.SeekingNotSupported); |
| | | 2245 | | } |
| | | 2246 | | } |
| | | 2247 | | public override long Position |
| | | 2248 | | { |
| | | 2249 | | get |
| | 0 | 2250 | | { |
| | 0 | 2251 | | ThrowIfDisposed(); |
| | 0 | 2252 | | return _position; |
| | 0 | 2253 | | } |
| | | 2254 | | set |
| | 0 | 2255 | | { |
| | 0 | 2256 | | ThrowIfDisposed(); |
| | 0 | 2257 | | throw new NotSupportedException(SR.SeekingNotSupported); |
| | | 2258 | | } |
| | | 2259 | | } |
| | | 2260 | | |
| | 0 | 2261 | | public override bool CanRead => false; |
| | 0 | 2262 | | public override bool CanSeek => false; |
| | 0 | 2263 | | public override bool CanWrite => _canWrite; |
| | | 2264 | | |
| | | 2265 | | private void ThrowIfDisposed() |
| | 0 | 2266 | | { |
| | 0 | 2267 | | if (_isDisposed) |
| | 0 | 2268 | | { |
| | 0 | 2269 | | throw new ObjectDisposedException(GetType().ToString(), SR.HiddenStreamName); |
| | | 2270 | | } |
| | 0 | 2271 | | } |
| | | 2272 | | |
| | | 2273 | | public override int Read(byte[] buffer, int offset, int count) |
| | 0 | 2274 | | { |
| | 0 | 2275 | | ThrowIfDisposed(); |
| | 0 | 2276 | | throw new NotSupportedException(SR.ReadingNotSupported); |
| | | 2277 | | } |
| | | 2278 | | |
| | | 2279 | | public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToke |
| | 0 | 2280 | | { |
| | 0 | 2281 | | ThrowIfDisposed(); |
| | 0 | 2282 | | throw new NotSupportedException(SR.ReadingNotSupported); |
| | | 2283 | | } |
| | | 2284 | | |
| | | 2285 | | public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) |
| | 0 | 2286 | | { |
| | 0 | 2287 | | ThrowIfDisposed(); |
| | 0 | 2288 | | throw new NotSupportedException(SR.ReadingNotSupported); |
| | | 2289 | | } |
| | | 2290 | | |
| | | 2291 | | public override long Seek(long offset, SeekOrigin origin) |
| | 0 | 2292 | | { |
| | 0 | 2293 | | ThrowIfDisposed(); |
| | 0 | 2294 | | throw new NotSupportedException(SR.SeekingNotSupported); |
| | | 2295 | | } |
| | | 2296 | | |
| | | 2297 | | public override void SetLength(long value) |
| | 0 | 2298 | | { |
| | 0 | 2299 | | ThrowIfDisposed(); |
| | 0 | 2300 | | throw new NotSupportedException(SR.SetLengthRequiresSeekingAndWriting); |
| | | 2301 | | } |
| | | 2302 | | |
| | | 2303 | | // careful: assumes that write is the only way to write to the stream, if writebyte/beginwrite are implement |
| | | 2304 | | // they must set _everWritten, etc. |
| | | 2305 | | public override void Write(byte[] buffer, int offset, int count) |
| | 0 | 2306 | | { |
| | 0 | 2307 | | ValidateBufferArguments(buffer, offset, count); |
| | | 2308 | | |
| | 0 | 2309 | | ThrowIfDisposed(); |
| | 0 | 2310 | | Debug.Assert(CanWrite); |
| | | 2311 | | |
| | | 2312 | | // if we're not actually writing anything, we don't want to trigger the header |
| | 0 | 2313 | | if (count == 0) |
| | 0 | 2314 | | { |
| | 0 | 2315 | | return; |
| | | 2316 | | } |
| | | 2317 | | |
| | 0 | 2318 | | if (!_everWritten) |
| | 0 | 2319 | | { |
| | 0 | 2320 | | _everWritten = true; |
| | | 2321 | | // write local header, we are good to go |
| | 0 | 2322 | | _usedZip64inLH = _entry.WriteLocalFileHeader(isEmptyFile: false, forceWrite: true); |
| | 0 | 2323 | | } |
| | | 2324 | | |
| | 0 | 2325 | | _crcSizeStream.Write(buffer, offset, count); |
| | 0 | 2326 | | _position += count; |
| | 0 | 2327 | | } |
| | | 2328 | | |
| | | 2329 | | public override void Write(ReadOnlySpan<byte> source) |
| | 0 | 2330 | | { |
| | 0 | 2331 | | ThrowIfDisposed(); |
| | 0 | 2332 | | Debug.Assert(CanWrite); |
| | | 2333 | | |
| | | 2334 | | // if we're not actually writing anything, we don't want to trigger the header |
| | 0 | 2335 | | if (source.Length == 0) |
| | 0 | 2336 | | { |
| | 0 | 2337 | | return; |
| | | 2338 | | } |
| | | 2339 | | |
| | 0 | 2340 | | if (!_everWritten) |
| | 0 | 2341 | | { |
| | 0 | 2342 | | _everWritten = true; |
| | | 2343 | | // write local header, we are good to go |
| | 0 | 2344 | | _usedZip64inLH = _entry.WriteLocalFileHeader(isEmptyFile: false, forceWrite: true); |
| | 0 | 2345 | | } |
| | | 2346 | | |
| | 0 | 2347 | | _crcSizeStream.Write(source); |
| | 0 | 2348 | | _position += source.Length; |
| | 0 | 2349 | | } |
| | | 2350 | | |
| | | 2351 | | public override void WriteByte(byte value) => |
| | 0 | 2352 | | Write(new ReadOnlySpan<byte>(in value)); |
| | | 2353 | | |
| | | 2354 | | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) |
| | 0 | 2355 | | { |
| | 0 | 2356 | | ValidateBufferArguments(buffer, offset, count); |
| | 0 | 2357 | | return WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).AsTask(); |
| | 0 | 2358 | | } |
| | | 2359 | | |
| | | 2360 | | public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = defa |
| | 0 | 2361 | | { |
| | 0 | 2362 | | ThrowIfDisposed(); |
| | 0 | 2363 | | Debug.Assert(CanWrite); |
| | | 2364 | | |
| | 0 | 2365 | | return !buffer.IsEmpty ? |
| | 0 | 2366 | | Core(buffer, cancellationToken) : |
| | 0 | 2367 | | default; |
| | | 2368 | | |
| | | 2369 | | async ValueTask Core(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken) |
| | 0 | 2370 | | { |
| | 0 | 2371 | | if (!_everWritten) |
| | 0 | 2372 | | { |
| | 0 | 2373 | | _everWritten = true; |
| | | 2374 | | // write local header, we are good to go |
| | 0 | 2375 | | _usedZip64inLH = await _entry.WriteLocalFileHeaderAsync(isEmptyFile: false, forceWrite: true, pr |
| | 0 | 2376 | | } |
| | | 2377 | | |
| | 0 | 2378 | | await _crcSizeStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); |
| | 0 | 2379 | | _position += buffer.Length; |
| | 0 | 2380 | | } |
| | 0 | 2381 | | } |
| | | 2382 | | |
| | | 2383 | | public override void Flush() |
| | 0 | 2384 | | { |
| | 0 | 2385 | | ThrowIfDisposed(); |
| | 0 | 2386 | | Debug.Assert(CanWrite); |
| | | 2387 | | |
| | 0 | 2388 | | _crcSizeStream.Flush(); |
| | 0 | 2389 | | } |
| | | 2390 | | |
| | | 2391 | | public override Task FlushAsync(CancellationToken cancellationToken) |
| | 0 | 2392 | | { |
| | 0 | 2393 | | ThrowIfDisposed(); |
| | 0 | 2394 | | Debug.Assert(CanWrite); |
| | | 2395 | | |
| | 0 | 2396 | | return _crcSizeStream.FlushAsync(cancellationToken); |
| | 0 | 2397 | | } |
| | | 2398 | | |
| | | 2399 | | protected override void Dispose(bool disposing) |
| | 0 | 2400 | | { |
| | 0 | 2401 | | if (disposing && !_isDisposed) |
| | 0 | 2402 | | { |
| | 0 | 2403 | | _crcSizeStream.Dispose(); // now we have size/crc info |
| | | 2404 | | |
| | | 2405 | | // If no data was written through CheckSumAndSizeWriteStream, its lazy _baseStream |
| | | 2406 | | // (DeflateStream wrapping the encryption stream) was never created, so the encryption |
| | | 2407 | | // stream would be orphaned. Dispose it explicitly to finalize encryption |
| | | 2408 | | // (e.g., write the ZipCrypto 12-byte header or AES salt/verifier/HMAC). |
| | 0 | 2409 | | if (!_everWritten) |
| | 0 | 2410 | | { |
| | 0 | 2411 | | _encryptionStream?.Dispose(); |
| | | 2412 | | |
| | | 2413 | | // write local header, no data, so we use stored |
| | 0 | 2414 | | _entry.WriteLocalFileHeader(isEmptyFile: true, forceWrite: true); |
| | 0 | 2415 | | } |
| | | 2416 | | else |
| | 0 | 2417 | | { |
| | | 2418 | | // go back and finish writing |
| | 0 | 2419 | | if (_entry._archive.ArchiveStream.CanSeek) |
| | 0 | 2420 | | { |
| | | 2421 | | // finish writing local header if we have seek capabilities |
| | 0 | 2422 | | _entry.WriteCrcAndSizesInLocalHeader(_usedZip64inLH); |
| | | 2423 | | |
| | | 2424 | | // ZipCrypto entries retain DataDescriptor for check byte correctness; |
| | | 2425 | | // write the trailing descriptor so the archive is consistent. |
| | 0 | 2426 | | if ((_entry._generalPurposeBitFlag & BitFlagValues.DataDescriptor) != 0) |
| | 0 | 2427 | | { |
| | 0 | 2428 | | _entry.WriteDataDescriptor(); |
| | 0 | 2429 | | } |
| | 0 | 2430 | | } |
| | | 2431 | | else |
| | | 2432 | | // write out data descriptor if we don't have seek capabilities |
| | 0 | 2433 | | _entry.WriteDataDescriptor(); |
| | 0 | 2434 | | } |
| | 0 | 2435 | | _canWrite = false; |
| | 0 | 2436 | | _isDisposed = true; |
| | 0 | 2437 | | } |
| | | 2438 | | |
| | 0 | 2439 | | base.Dispose(disposing); |
| | 0 | 2440 | | } |
| | | 2441 | | |
| | | 2442 | | public override async ValueTask DisposeAsync() |
| | 0 | 2443 | | { |
| | 0 | 2444 | | if (!_isDisposed) |
| | 0 | 2445 | | { |
| | 0 | 2446 | | await _crcSizeStream.DisposeAsync().ConfigureAwait(false); // now we have size/crc info |
| | | 2447 | | |
| | | 2448 | | // If no data was written through CheckSumAndSizeWriteStream, its lazy _baseStream |
| | | 2449 | | // (DeflateStream wrapping the encryption stream) was never created, so the encryption |
| | | 2450 | | // stream would be orphaned. Dispose it explicitly to finalize encryption |
| | | 2451 | | // (e.g., write the ZipCrypto 12-byte header or AES salt/verifier/HMAC). |
| | 0 | 2452 | | if (!_everWritten) |
| | 0 | 2453 | | { |
| | 0 | 2454 | | if (_encryptionStream is not null) |
| | 0 | 2455 | | { |
| | 0 | 2456 | | await _encryptionStream.DisposeAsync().ConfigureAwait(false); |
| | 0 | 2457 | | } |
| | | 2458 | | |
| | | 2459 | | // write local header, no data, so we use stored |
| | 0 | 2460 | | await _entry.WriteLocalFileHeaderAsync(isEmptyFile: true, forceWrite: true, preserveDataDescript |
| | 0 | 2461 | | } |
| | | 2462 | | else |
| | 0 | 2463 | | { |
| | | 2464 | | // go back and finish writing |
| | 0 | 2465 | | if (_entry._archive.ArchiveStream.CanSeek) |
| | 0 | 2466 | | { |
| | | 2467 | | // finish writing local header if we have seek capabilities |
| | 0 | 2468 | | await _entry.WriteCrcAndSizesInLocalHeaderAsync(_usedZip64inLH, cancellationToken: default). |
| | | 2469 | | |
| | | 2470 | | // ZipCrypto entries retain DataDescriptor for check byte correctness; |
| | | 2471 | | // write the trailing descriptor so the archive is consistent. |
| | 0 | 2472 | | if ((_entry._generalPurposeBitFlag & BitFlagValues.DataDescriptor) != 0) |
| | 0 | 2473 | | { |
| | 0 | 2474 | | await _entry.WriteDataDescriptorAsync(cancellationToken: default).ConfigureAwait(false); |
| | 0 | 2475 | | } |
| | 0 | 2476 | | } |
| | | 2477 | | else |
| | | 2478 | | // write out data descriptor if we don't have seek capabilities |
| | 0 | 2479 | | await _entry.WriteDataDescriptorAsync(cancellationToken: default).ConfigureAwait(false); |
| | 0 | 2480 | | } |
| | 0 | 2481 | | _canWrite = false; |
| | 0 | 2482 | | _isDisposed = true; |
| | 0 | 2483 | | } |
| | | 2484 | | |
| | 0 | 2485 | | await base.DisposeAsync().ConfigureAwait(false); |
| | 0 | 2486 | | } |
| | | 2487 | | } |
| | | 2488 | | [Flags] |
| | | 2489 | | internal enum BitFlagValues : ushort |
| | | 2490 | | { |
| | | 2491 | | IsEncrypted = 0x1, |
| | | 2492 | | DataDescriptor = 0x8, |
| | | 2493 | | StrongEncryption = 0x40, |
| | | 2494 | | UnicodeFileNameAndComment = 0x800 |
| | | 2495 | | } |
| | | 2496 | | |
| | | 2497 | | internal sealed class LocalHeaderOffsetComparer : Comparer<ZipArchiveEntry> |
| | | 2498 | | { |
| | 0 | 2499 | | private static readonly LocalHeaderOffsetComparer s_instance = new LocalHeaderOffsetComparer(); |
| | | 2500 | | |
| | 0 | 2501 | | public static LocalHeaderOffsetComparer Instance => s_instance; |
| | | 2502 | | |
| | | 2503 | | // Newly added ZipArchiveEntry records should always go to the end of the file. |
| | | 2504 | | public override int Compare(ZipArchiveEntry? x, ZipArchiveEntry? y) |
| | 0 | 2505 | | { |
| | 0 | 2506 | | long xOffset = x != null && !x.OriginallyInArchive ? long.MaxValue : x?.OffsetOfLocalHeader ?? long.MinV |
| | 0 | 2507 | | long yOffset = y != null && !y.OriginallyInArchive ? long.MaxValue : y?.OffsetOfLocalHeader ?? long.MinV |
| | | 2508 | | |
| | 0 | 2509 | | return xOffset.CompareTo(yOffset); |
| | 0 | 2510 | | } |
| | | 2511 | | } |
| | | 2512 | | } |
| | | 2513 | | } |