< Summary

Line coverage
46%
Covered lines: 364
Uncovered lines: 411
Coverable lines: 775
Total lines: 1562
Line coverage: 46.9%
Branch coverage
35%
Covered branches: 98
Total branches: 280
Branch coverage: 35%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
File 1: CreateAsync(...)31.25%161641.86%
File 1: DisposeAsync()100%11100%
File 1: DisposeAsyncCore()21.42%141448.14%
File 1: CloseStreamsAsync()33.33%6643.75%
File 1: EnsureCentralDirectoryReadAsync(...)50%22100%
File 1: ReadCentralDirectoryAsync(...)100%8891.42%
File 1: ReadEndOfCentralDirectoryAsync(...)100%4487.5%
File 1: TryReadZip64EndOfCentralDirectoryAsync(...)100%101095.23%
File 1: WriteFileAsync(...)0%22220%
File 1: WriteArchiveEpilogueAsync(...)0%12120%
File 2: .ctor(...)100%110%
File 2: .ctor(...)100%110%
File 2: .ctor(...)100%110%
File 2: .ctor(...)25%121243.58%
File 2: .ctor(...)100%11100%
File 2: CreateEntry(...)100%110%
File 2: CreateEntry(...)100%110%
File 2: CreateEntry(...)100%110%
File 2: CreateEntry(...)100%110%
File 2: Dispose(...)25%121248.14%
File 2: Dispose()100%11100%
File 2: GetEntry(...)0%220%
File 2: DoCreateEntry(...)0%440%
File 2: AcquireArchiveStream(...)0%440%
File 2: AddEntry(...)100%11100%
File 2: DebugAssertIsStillArchiveStreamOwner(...)100%110%
File 2: ReleaseArchiveStream(...)100%110%
File 2: RemoveEntry(...)0%440%
File 2: ThrowIfDisposed()100%11100%
File 2: CloseStreams()33.33%6653.84%
File 2: EnsureCentralDirectoryRead()100%22100%
File 2: ReadCentralDirectoryInitialize(...)100%11100%
File 2: ReadCentralDirectoryEndOfInnerLoopWork(...)100%44100%
File 2: ReadCentralDirectoryEndOfOuterLoopWork(...)100%22100%
File 2: ReadCentralDirectoryPostOuterLoopWork(...)37.5%8840%
File 2: ReadCentralDirectory()100%8891.17%
File 2: ReadEndOfCentralDirectoryInnerWork(...)100%44100%
File 2: ReadEndOfCentralDirectory()100%4486.36%
File 2: TryReadZip64EndOfCentralDirectoryInnerInitialWork(...)75%88100%
File 2: TryReadZip64EndOfCentralDirectoryInnerFinalWork(...)83.33%6678.57%
File 2: TryReadZip64EndOfCentralDirectory(...)100%101095%
File 2: WriteFileCalculateOffsets(...)0%220%
File 2: WriteFileCheckStartingOffset(...)0%220%
File 2: WriteFileUpdateModeFinalWork(...)0%220%
File 2: WriteFileFinalWork()0%440%
File 2: WriteFile()0%22220%
File 2: WriteArchiveEpilogueNoCDChangesWork()100%110%
File 2: WriteArchiveEpilogue(...)0%12120%
File 2: ValidateMode(...)31.25%161636.36%
File 2: DecideArchiveStream(...)25%44100%

File(s)

D:\runner\runtime\src\libraries\System.IO.Compression\src\System\IO\Compression\ZipArchive.Async.cs

#LineLine coverage
 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
 4using System.Buffers;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Text;
 8using System.Threading;
 9using System.Threading.Tasks;
 10
 11namespace System.IO.Compression;
 12
 13public partial class ZipArchive : IDisposable, IAsyncDisposable
 14{
 15    /// <summary>
 16    /// Asynchronously initializes and returns a new instance of <see cref="ZipArchive"/> on the given stream in the spe
 17    /// </summary>
 18    /// <param name="stream">The input or output stream.</param>
 19    /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support reading, 
 20    /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param>
 21    /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names and comments in this Zip
 22    ///         ///     <para>NOTE: Specifying this parameter to values other than <c>null</c> is discouraged.
 23    ///         However, this may be necessary for interoperability with ZIP archive tools and libraries that do not cor
 24    ///         UTF-8 encoding for entry names.<br />
 25    ///         This value is used as follows:</para>
 26    ///     <para><strong>Reading (opening) ZIP archive files:</strong></para>
 27    ///     <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para>
 28    ///     <list>
 29    ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local fi
 30    ///         use the current system default code page (<c>Encoding.Default</c>) in order to decode the entry name and
 31    ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local fi
 32    ///         use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name and comment.</item>
 33    ///     </list>
 34    ///     <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para>
 35    ///     <list>
 36    ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local fi
 37    ///         use the specified <c>entryNameEncoding</c> in order to decode the entry name and comment.</item>
 38    ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the local fi
 39    ///         use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name and comment.</item>
 40    ///     </list>
 41    ///     <para><strong>Writing (saving) ZIP archive files:</strong></para>
 42    ///     <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para>
 43    ///     <list>
 44    ///         <item>For entry names and comments that contain characters outside the ASCII range,
 45    ///         the language encoding flag (EFS) will be set in the general purpose bit flag of the local file header,
 46    ///         and UTF-8 (<c>Encoding.UTF8</c>) will be used in order to encode the entry name and comment into bytes.<
 47    ///         <item>For entry names and comments that do not contain characters outside the ASCII range,
 48    ///         the language encoding flag (EFS) will not be set in the general purpose bit flag of the local file heade
 49    ///         and the current system default code page (<c>Encoding.Default</c>) will be used to encode the entry name
 50    ///     </list>
 51    ///     <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para>
 52    ///     <list>
 53    ///         <item>The specified <c>entryNameEncoding</c> will always be used to encode the entry names and comments 
 54    ///         The language encoding flag (EFS) in the general purpose bit flag of the local file header will be set if
 55    ///         if the specified <c>entryNameEncoding</c> is a UTF-8 encoding.</item>
 56    ///     </list>
 57    ///     <para>Note that Unicode encodings other than UTF-8 may not be currently used for the <c>entryNameEncoding</c
 58    ///     otherwise an <see cref="ArgumentException"/> is thrown.</para>
 59    /// </param>
 60    /// <param name="cancellationToken">The optional cancellation token to monitor.</param>
 61    /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabilitie
 62    /// <exception cref="ArgumentNullException">The stream is null.</exception>
 63    /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception>
 64    /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -or- m
 65    /// <exception cref="ArgumentException">If a Unicode encoding other than UTF-8 is specified for the <code>entryNameE
 66    /// <returns>A task that represents the asynchronous initialization. The task result is a <see cref="ZipArchive"/> i
 67    public static async Task<ZipArchive> CreateAsync(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? entry
 173868    {
 173869        cancellationToken.ThrowIfCancellationRequested();
 70
 173871        ArgumentNullException.ThrowIfNull(stream);
 72
 173873        Stream? extraTempStream = null;
 74
 75        try
 173876        {
 173877            Stream? backingStream = null;
 78
 173879            if (ValidateMode(mode, stream))
 080            {
 081                backingStream = stream;
 082                extraTempStream = stream = new MemoryStream();
 083                await backingStream.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
 084                stream.Seek(0, SeekOrigin.Begin);
 085            }
 86
 173887            ZipArchive zipArchive = new(mode, leaveOpen, entryNameEncoding, backingStream, DecideArchiveStream(mode, str
 88
 173889            switch (mode)
 90            {
 91                case ZipArchiveMode.Create:
 092                    zipArchive._readEntries = true;
 093                    break;
 94                case ZipArchiveMode.Read:
 173895                    await zipArchive.ReadEndOfCentralDirectoryAsync(cancellationToken).ConfigureAwait(false);
 96
 97                    // As there is no API for accessing .Entries asynchronously, we are expected to read the central
 98                    // directory up-front
 160599                    await zipArchive.EnsureCentralDirectoryReadAsync(cancellationToken).ConfigureAwait(false);
 42100                    break;
 101                case ZipArchiveMode.Update:
 102                default:
 0103                    Debug.Assert(mode == ZipArchiveMode.Update);
 0104                    if (zipArchive._archiveStream.Length == 0)
 0105                    {
 0106                        zipArchive._readEntries = true;
 0107                    }
 108                    else
 0109                    {
 0110                        await zipArchive.ReadEndOfCentralDirectoryAsync(cancellationToken).ConfigureAwait(false);
 0111                        await zipArchive.EnsureCentralDirectoryReadAsync(cancellationToken).ConfigureAwait(false);
 112
 0113                        foreach (ZipArchiveEntry entry in zipArchive._entries)
 0114                        {
 0115                            await entry.ThrowIfNotOpenableAsync(needToUncompress: false, needToLoadIntoMemory: true, can
 0116                        }
 0117                    }
 0118                    break;
 119            }
 120
 42121            return zipArchive;
 122        }
 1696123        catch (Exception)
 1696124        {
 1696125            if (extraTempStream != null)
 0126            {
 0127                await extraTempStream.DisposeAsync().ConfigureAwait(false);
 0128            }
 129
 1696130            throw;
 131        }
 42132    }
 133
 134    /// <summary>
 135    /// Asynchronously releases the resources used by the <see cref="ZipArchive"/>.
 136    /// </summary>
 137    /// <returns>A <see cref="ValueTask"/> that represents the asynchronous dispose operation.</returns>
 42138    public async ValueTask DisposeAsync() => await DisposeAsyncCore().ConfigureAwait(false);
 139
 140    protected virtual async ValueTask DisposeAsyncCore()
 42141    {
 42142        if (!_isDisposed)
 42143        {
 144            try
 42145            {
 42146                switch (_mode)
 147                {
 148                    case ZipArchiveMode.Read:
 42149                        break;
 150                    case ZipArchiveMode.Create:
 0151                        await WriteFileAsync().ConfigureAwait(false);
 0152                        break;
 153                    case ZipArchiveMode.Update:
 154                    default:
 0155                        Debug.Assert(_mode == ZipArchiveMode.Update);
 156                        // Only write if the archive has been modified
 0157                        if (IsModified)
 0158                        {
 0159                            await WriteFileAsync().ConfigureAwait(false);
 0160                        }
 161                        else
 0162                        {
 163                            // Even if we didn't write, unload any entry buffers that may have been loaded
 0164                            foreach (ZipArchiveEntry entry in _entries)
 0165                            {
 0166                                await entry.UnloadStreamsAsync().ConfigureAwait(false);
 0167                            }
 0168                        }
 0169                        break;
 170                }
 42171            }
 172            finally
 42173            {
 42174                await CloseStreamsAsync().ConfigureAwait(false);
 42175                _isDisposed = true;
 42176            }
 42177        }
 42178    }
 179
 180    private async Task CloseStreamsAsync()
 42181    {
 42182        if (!_leaveOpen)
 42183        {
 42184            await _archiveStream.DisposeAsync().ConfigureAwait(false);
 42185            if (_backingStream != null)
 0186            {
 0187                await _backingStream.DisposeAsync().ConfigureAwait(false);
 0188            }
 42189        }
 190        else
 0191        {
 192            // if _backingStream isn't null, that means we assigned the original stream they passed
 193            // us to _backingStream (which they requested we leave open), and _archiveStream was
 194            // the temporary copy that we needed
 0195            if (_backingStream != null)
 0196            {
 0197                await _archiveStream.DisposeAsync().ConfigureAwait(false);
 0198            }
 0199        }
 42200    }
 201
 202    private async Task EnsureCentralDirectoryReadAsync(CancellationToken cancellationToken)
 1605203    {
 1605204        cancellationToken.ThrowIfCancellationRequested();
 205
 1605206        if (!_readEntries)
 1605207        {
 1605208            await ReadCentralDirectoryAsync(cancellationToken).ConfigureAwait(false);
 42209            _readEntries = true;
 42210        }
 42211    }
 212
 213    private async Task ReadCentralDirectoryAsync(CancellationToken cancellationToken)
 1605214    {
 1605215        cancellationToken.ThrowIfCancellationRequested();
 216
 1605217        byte[] arrayPoolArray = ArrayPool<byte>.Shared.Rent(ReadCentralDirectoryReadBufferSize);
 218        try
 1605219        {
 1605220            Memory<byte> fileBuffer = arrayPoolArray;
 221
 1605222            ReadCentralDirectoryInitialize(out long numberOfEntries, out bool saveExtraFieldsAndComments, out bool conti
 223
 224            // read the central directory
 3501225            while (continueReadingCentralDirectory)
 2239226            {
 227                // the buffer read must always be large enough to fit the constant section size of at least one header
 2239228                int currBytesRead = await _archiveStream.ReadAtLeastAsync(fileBuffer, ZipCentralDirectoryFileHeader.Bloc
 229
 2239230                ReadOnlyMemory<byte> sizedFileBuffer = fileBuffer[0..currBytesRead];
 2239231                continueReadingCentralDirectory = currBytesRead >= ZipCentralDirectoryFileHeader.BlockConstantSectionSiz
 232
 16837233                while (currPosition + ZipCentralDirectoryFileHeader.BlockConstantSectionSize <= currBytesRead)
 15556234                {
 15556235                    (bool result, bytesConsumed, ZipCentralDirectoryFileHeader? currentHeader) =
 15556236                        await ZipCentralDirectoryFileHeader.TryReadBlockAsync(sizedFileBuffer.Slice(currPosition), _arch
 237
 15493238                    if (!ReadCentralDirectoryEndOfInnerLoopWork(result, currentHeader, bytesConsumed, ref continueReadin
 615239                    {
 615240                        break;
 241                    }
 242
 14598243                    ZipArchiveEntry lastEntry = _entries[_entries.Count - 1];
 14598244                    if (lastEntry.IsEncrypted)
 903245                    {
 903246                        await lastEntry.ReadEncryptionSaltIfNeededAsync(cancellationToken).ConfigureAwait(false);
 903247                    }
 14598248                }
 249
 1896250                ReadCentralDirectoryEndOfOuterLoopWork(ref currPosition, sizedFileBuffer.Span);
 1896251            }
 252
 1262253            ReadCentralDirectoryPostOuterLoopWork(numberOfEntries);
 42254        }
 0255        catch (EndOfStreamException ex)
 0256        {
 0257            throw new InvalidDataException(SR.CentralDirectoryInvalid, ex);
 258        }
 259        finally
 1605260        {
 1605261            ArrayPool<byte>.Shared.Return(arrayPoolArray);
 1605262        }
 42263    }
 264
 265    // This function reads all the EOCD stuff it needs to find the offset to the start of the central directory
 266    // This offset gets put in _centralDirectoryStart and the number of this disk gets put in _numberOfThisDisk
 267    // Also does some verification that this isn't a split/spanned archive
 268    // Also checks that offset to CD isn't out of bounds
 269    private async Task ReadEndOfCentralDirectoryAsync(CancellationToken cancellationToken)
 1738270    {
 1738271        cancellationToken.ThrowIfCancellationRequested();
 272
 273        try
 1738274        {
 275            // This seeks backwards almost to the beginning of the EOCD, one byte after where the signature would be
 276            // located if the EOCD had the minimum possible size (no file zip comment)
 1738277            _archiveStream.Seek(-ZipEndOfCentralDirectoryBlock.SizeOfBlockWithoutSignature, SeekOrigin.End);
 278
 279            // If the EOCD has the minimum possible size (no zip file comment), then exactly the previous 4 bytes will c
 280            // But if the EOCD has max possible size, the signature should be found somewhere in the previous 64K + 4 by
 1735281            if (!await ZipHelper.SeekBackwardsToSignatureAsync(_archiveStream,
 1735282                    ZipEndOfCentralDirectoryBlock.SignatureConstantBytes,
 1735283                    ZipEndOfCentralDirectoryBlock.ZipFileCommentMaxLength + ZipEndOfCentralDirectoryBlock.FieldLengths.S
 1735284                    cancellationToken).ConfigureAwait(false))
 6285                throw new InvalidDataException(SR.EOCDNotFound);
 286
 1729287            long eocdStart = _archiveStream.Position;
 288
 289            // read the EOCD
 1729290            ZipEndOfCentralDirectoryBlock eocd = await ZipEndOfCentralDirectoryBlock.ReadBlockAsync(_archiveStream, canc
 291
 1723292            ReadEndOfCentralDirectoryInnerWork(eocd);
 293
 1715294            await TryReadZip64EndOfCentralDirectoryAsync(eocd, eocdStart, cancellationToken).ConfigureAwait(false);
 295
 1614296            if (_centralDirectoryStart > _archiveStream.Length)
 9297            {
 9298                throw new InvalidDataException(SR.FieldTooBigOffsetToCD);
 299            }
 1605300        }
 0301        catch (EndOfStreamException ex)
 0302        {
 0303            throw new InvalidDataException(SR.CDCorrupt, ex);
 304        }
 3305        catch (IOException ex)
 3306        {
 3307            throw new InvalidDataException(SR.CDCorrupt, ex);
 308        }
 1605309    }
 310
 311    // Tries to find the Zip64 End of Central Directory Locator, then the Zip64 End of Central Directory, assuming the
 312    // End of Central Directory block has already been found, as well as the location in the stream where the EOCD start
 313    private async ValueTask TryReadZip64EndOfCentralDirectoryAsync(ZipEndOfCentralDirectoryBlock eocd, long eocdStart, C
 1715314    {
 1715315        cancellationToken.ThrowIfCancellationRequested();
 316
 317        // Only bother looking for the Zip64-EOCD stuff if we suspect it is needed because some value is FFFFFFFFF
 318        // because these are the only two values we need, we only worry about these
 319        // if we don't find the Zip64-EOCD, we just give up and try to use the original values
 1715320        if (eocd.NumberOfThisDisk == ZipHelper.Mask16Bit ||
 1715321            eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber == ZipHelper.Mask32Bit ||
 1715322            eocd.NumberOfEntriesInTheCentralDirectory == ZipHelper.Mask16Bit)
 1292323        {
 324            // Read Zip64 End of Central Directory Locator
 325
 326            // Check if there's enough space before the EOCD to look for the Zip64 EOCDL
 1292327            if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize)
 12328            {
 12329                throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
 330            }
 331
 332            // This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature would 
 1280333            _archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Be
 334
 335            // Exactly the previous 4 bytes should contain the Zip64-EOCDL signature
 336            // if we don't find it, assume it doesn't exist and use data from normal EOCD
 1280337            if (await ZipHelper.SeekBackwardsToSignatureAsync(_archiveStream,
 1280338                    Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes,
 1280339                    Zip64EndOfCentralDirectoryLocator.FieldLengths.Signature, cancellationToken).ConfigureAwait(false))
 89340            {
 341                // use locator to get to Zip64-EOCD
 89342                Zip64EndOfCentralDirectoryLocator locator = await Zip64EndOfCentralDirectoryLocator.TryReadBlockAsync(_a
 89343                TryReadZip64EndOfCentralDirectoryInnerInitialWork(locator);
 344
 345                // Read Zip64 End of Central Directory Record
 27346                Zip64EndOfCentralDirectoryRecord record = await Zip64EndOfCentralDirectoryRecord.TryReadBlockAsync(_arch
 347
 18348                TryReadZip64EndOfCentralDirectoryInnerFinalWork(record);
 0349            }
 1191350        }
 1614351    }
 352
 353    private async ValueTask WriteFileAsync(CancellationToken cancellationToken = default)
 0354    {
 0355        cancellationToken.ThrowIfCancellationRequested();
 356
 357        // if we are in create mode, we always set readEntries to true in Init
 358        // if we are in update mode, we call EnsureCentralDirectoryRead, which sets readEntries to true
 0359        Debug.Assert(_readEntries);
 360
 361        // Entries starting after this offset have had a dynamically-sized change. Everything on or after this point mus
 0362        long completeRewriteStartingOffset = 0;
 0363        List<ZipArchiveEntry> entriesToWrite = _entries;
 364
 0365        if (_mode == ZipArchiveMode.Update)
 0366        {
 367            // Entries starting after this offset have some kind of change made to them. It might just be a fixed-length
 368            // that single entry's metadata can be rewritten without impacting anything else.
 0369            long startingOffset = _firstDeletedEntryOffset;
 0370            long nextFileOffset = 0;
 0371            completeRewriteStartingOffset = startingOffset;
 372
 0373            entriesToWrite = new(_entries.Count);
 374
 0375            foreach (ZipArchiveEntry entry in _entries)
 0376            {
 0377                if (!entry.OriginallyInArchive)
 0378                {
 0379                    entriesToWrite.Add(entry);
 0380                }
 381                else
 0382                {
 0383                    WriteFileCalculateOffsets(entry, ref startingOffset, ref nextFileOffset);
 384
 385                    // We want to re-write entries which are after the starting offset of the first entry which has pend
 386                    // NB: the existing ZipArchiveEntries are sorted in _entries by their position ascending.
 0387                    if (entry.OffsetOfLocalHeader >= startingOffset)
 0388                    {
 0389                        WriteFileCheckStartingOffset(entry, ref completeRewriteStartingOffset);
 390
 0391                        await entry.LoadLocalHeaderExtraFieldIfNeededAsync(cancellationToken).ConfigureAwait(false);
 0392                        if (entry.OffsetOfLocalHeader >= completeRewriteStartingOffset)
 0393                        {
 0394                            await entry.LoadCompressedBytesIfNeededAsync(cancellationToken).ConfigureAwait(false);
 0395                        }
 396
 0397                        entriesToWrite.Add(entry);
 0398                    }
 0399                }
 0400            }
 401
 0402            WriteFileUpdateModeFinalWork(startingOffset, nextFileOffset);
 0403        }
 404
 0405        foreach (ZipArchiveEntry entry in entriesToWrite)
 0406        {
 407            // We don't always need to write the local header entry, ZipArchiveEntry is usually able to work out when it
 408            // We want to force this header entry to be written (even for completely untouched entries) if the entry com
 409            // which had a pending dynamically-sized write.
 0410            bool forceWriteLocalEntry = !entry.OriginallyInArchive || (entry.OriginallyInArchive && entry.OffsetOfLocalH
 411
 0412            await entry.WriteAndFinishLocalEntryAsync(forceWriteLocalEntry, cancellationToken).ConfigureAwait(false);
 0413        }
 414
 0415        long plannedCentralDirectoryPosition = _archiveStream.Position;
 416        // If there are no entries in the archive, we still want to create the archive epilogue.
 0417        bool archiveEpilogueRequiresUpdate = _entries.Count == 0;
 418
 0419        foreach (ZipArchiveEntry entry in _entries)
 0420        {
 421            // The central directory needs to be rewritten if its position has moved, if there's a new entry in the arch
 0422            bool centralDirectoryEntryRequiresUpdate = plannedCentralDirectoryPosition != _centralDirectoryStart
 0423                || !entry.OriginallyInArchive || entry.OffsetOfLocalHeader >= completeRewriteStartingOffset;
 424
 0425            await entry.WriteCentralDirectoryFileHeaderAsync(centralDirectoryEntryRequiresUpdate, cancellationToken).Con
 0426            archiveEpilogueRequiresUpdate |= centralDirectoryEntryRequiresUpdate;
 0427        }
 428
 0429        long sizeOfCentralDirectory = _archiveStream.Position - plannedCentralDirectoryPosition;
 430
 0431        await WriteArchiveEpilogueAsync(plannedCentralDirectoryPosition, sizeOfCentralDirectory, archiveEpilogueRequires
 432
 0433        WriteFileFinalWork();
 0434    }
 435
 436    // writes eocd, and if needed, zip 64 eocd, zip64 eocd locator
 437    // should only throw an exception in extremely exceptional cases because it is called from dispose
 438    private async ValueTask WriteArchiveEpilogueAsync(long startOfCentralDirectory, long sizeOfCentralDirectory, bool ce
 0439    {
 0440        cancellationToken.ThrowIfCancellationRequested();
 441
 442        // determine if we need Zip 64
 0443        if (startOfCentralDirectory >= uint.MaxValue
 0444            || sizeOfCentralDirectory >= uint.MaxValue
 0445            || _entries.Count >= ZipHelper.Mask16Bit
 0446#if DEBUG_FORCE_ZIP64
 0447                || _forceZip64
 0448#endif
 0449            )
 0450        {
 451            // if we need zip 64, write zip 64 eocd and locator
 0452            long zip64EOCDRecordStart = _archiveStream.Position;
 453
 0454            if (centralDirectoryChanged)
 0455            {
 0456                await Zip64EndOfCentralDirectoryRecord.WriteBlockAsync(_archiveStream, _entries.Count, startOfCentralDir
 0457                await Zip64EndOfCentralDirectoryLocator.WriteBlockAsync(_archiveStream, zip64EOCDRecordStart, cancellati
 0458            }
 459            else
 0460            {
 0461                WriteArchiveEpilogueNoCDChangesWork();
 0462            }
 0463        }
 464
 465        // write normal eocd
 0466        if (centralDirectoryChanged || (Changed != ChangeState.Unchanged))
 0467        {
 0468            await ZipEndOfCentralDirectoryBlock.WriteBlockAsync(_archiveStream, _entries.Count, startOfCentralDirectory,
 0469        }
 470        else
 0471        {
 0472            _archiveStream.Seek(ZipEndOfCentralDirectoryBlock.TotalSize + _archiveComment.Length, SeekOrigin.Current);
 0473        }
 0474    }
 475}

D:\runner\runtime\src\libraries\System.IO.Compression\src\System\IO\Compression\ZipArchive.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4
 5// Zip Spec here: http://www.pkware.com/documents/casestudies/APPNOTE.TXT
 6
 7using System.Buffers;
 8using System.Collections.Generic;
 9using System.Collections.ObjectModel;
 10using System.ComponentModel;
 11using System.Diagnostics;
 12using System.Diagnostics.CodeAnalysis;
 13using System.Text;
 14
 15namespace System.IO.Compression
 16{
 17    public partial class ZipArchive : IDisposable, IAsyncDisposable
 18    {
 19        private const int ReadCentralDirectoryReadBufferSize = 4096;
 20
 21        private readonly Stream _archiveStream;
 22        private ZipArchiveEntry? _archiveStreamOwner;
 23        private readonly ZipArchiveMode _mode;
 24        private readonly List<ZipArchiveEntry> _entries;
 25        private readonly ReadOnlyCollection<ZipArchiveEntry> _entriesCollection;
 26        private readonly Dictionary<string, ZipArchiveEntry> _entriesDictionary;
 27        private bool _readEntries;
 28        private readonly bool _leaveOpen;
 29        private long _centralDirectoryStart; // only valid after ReadCentralDirectory
 30        private bool _isDisposed;
 31        private uint _numberOfThisDisk; // only valid after ReadCentralDirectory
 32        private long _expectedNumberOfEntries;
 33        private readonly Stream? _backingStream;
 34        private byte[] _archiveComment;
 35        private Encoding? _entryNameAndCommentEncoding;
 36        private long _firstDeletedEntryOffset;
 37
 38#if DEBUG_FORCE_ZIP64
 39        public bool _forceZip64;
 40#endif
 41
 42        /// <summary>
 43        /// Initializes a new instance of ZipArchive on the given stream for reading.
 44        /// </summary>
 45        /// <exception cref="ArgumentException">The stream is already closed or does not support reading.</exception>
 46        /// <exception cref="ArgumentNullException">The stream is null.</exception>
 47        /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip archive.
 48        /// <param name="stream">The stream containing the archive to be read.</param>
 049        public ZipArchive(Stream stream) : this(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null) 
 50
 51        /// <summary>
 52        /// Initializes a new instance of ZipArchive on the given stream in the specified mode.
 53        /// </summary>
 54        /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabil
 55        /// <exception cref="ArgumentNullException">The stream is null.</exception>
 56        /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception>
 57        /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -o
 58        /// <param name="stream">The input or output stream.</param>
 59        /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support readi
 060        public ZipArchive(Stream stream, ZipArchiveMode mode) : this(stream, mode, leaveOpen: false, entryNameEncoding: 
 61
 62        /// <summary>
 63        /// Initializes a new instance of ZipArchive on the given stream in the specified mode, specifying whether to le
 64        /// </summary>
 65        /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabil
 66        /// <exception cref="ArgumentNullException">The stream is null.</exception>
 67        /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception>
 68        /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -o
 69        /// <param name="stream">The input or output stream.</param>
 70        /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support readi
 71        /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param
 072        public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen) : this(stream, mode, leaveOpen, entryNameE
 73
 74        /// <summary>
 75        /// Initializes a new instance of ZipArchive on the given stream in the specified mode, specifying whether to le
 76        /// </summary>
 77        /// <exception cref="ArgumentException">The stream is already closed. -or- mode is incompatible with the capabil
 78        /// <exception cref="ArgumentNullException">The stream is null.</exception>
 79        /// <exception cref="ArgumentOutOfRangeException">mode specified an invalid value.</exception>
 80        /// <exception cref="InvalidDataException">The contents of the stream could not be interpreted as a Zip file. -o
 81        /// <param name="stream">The input or output stream.</param>
 82        /// <param name="mode">See the description of the ZipArchiveMode enum. Read requires the stream to support readi
 83        /// <param name="leaveOpen">true to leave the stream open upon disposing the ZipArchive, otherwise false.</param
 84        /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names and comments in this
 85        ///         ///     <para>NOTE: Specifying this parameter to values other than <c>null</c> is discouraged.
 86        ///         However, this may be necessary for interoperability with ZIP archive tools and libraries that do not
 87        ///         UTF-8 encoding for entry names.<br />
 88        ///         This value is used as follows:</para>
 89        ///     <para><strong>Reading (opening) ZIP archive files:</strong></para>
 90        ///     <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para>
 91        ///     <list>
 92        ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca
 93        ///         use the current system default code page (<c>Encoding.Default</c>) in order to decode the entry name
 94        ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca
 95        ///         use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name and comment.</item>
 96        ///     </list>
 97        ///     <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para>
 98        ///     <list>
 99        ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca
 100        ///         use the specified <c>entryNameEncoding</c> in order to decode the entry name and comment.</item>
 101        ///         <item>For entries where the language encoding flag (EFS) in the general purpose bit flag of the loca
 102        ///         use UTF-8 (<c>Encoding.UTF8</c>) in order to decode the entry name and comment.</item>
 103        ///     </list>
 104        ///     <para><strong>Writing (saving) ZIP archive files:</strong></para>
 105        ///     <para>If <c>entryNameEncoding</c> is not specified (<c>== null</c>):</para>
 106        ///     <list>
 107        ///         <item>For entry names and comments that contain characters outside the ASCII range,
 108        ///         the language encoding flag (EFS) will be set in the general purpose bit flag of the local file heade
 109        ///         and UTF-8 (<c>Encoding.UTF8</c>) will be used in order to encode the entry name and comment into byt
 110        ///         <item>For entry names and comments that do not contain characters outside the ASCII range,
 111        ///         the language encoding flag (EFS) will not be set in the general purpose bit flag of the local file h
 112        ///         and the current system default code page (<c>Encoding.Default</c>) will be used to encode the entry 
 113        ///     </list>
 114        ///     <para>If <c>entryNameEncoding</c> is specified (<c>!= null</c>):</para>
 115        ///     <list>
 116        ///         <item>The specified <c>entryNameEncoding</c> will always be used to encode the entry names and comme
 117        ///         The language encoding flag (EFS) in the general purpose bit flag of the local file header will be se
 118        ///         if the specified <c>entryNameEncoding</c> is a UTF-8 encoding.</item>
 119        ///     </list>
 120        ///     <para>Note that Unicode encodings other than UTF-8 may not be currently used for the <c>entryNameEncodin
 121        ///     otherwise an <see cref="ArgumentException"/> is thrown.</para>
 122        /// </param>
 123        /// <exception cref="ArgumentException">If a Unicode encoding other than UTF-8 is specified for the <code>entryN
 124        public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding)
 1738125            : this(mode, leaveOpen, entryNameEncoding, backingStream: null, archiveStream: DecideArchiveStream(mode, str
 1738126        {
 1738127            ArgumentNullException.ThrowIfNull(stream);
 128
 1738129            Stream? extraTempStream = null;
 130
 131            try
 1738132            {
 1738133                _backingStream = null;
 134
 1738135                if (ValidateMode(mode, stream))
 0136                {
 0137                    _backingStream = stream;
 0138                    extraTempStream = stream = new MemoryStream();
 0139                    _backingStream.CopyTo(stream);
 0140                    stream.Seek(0, SeekOrigin.Begin);
 0141                }
 142
 1738143                _archiveStream = DecideArchiveStream(mode, stream);
 144
 1738145                switch (mode)
 146                {
 147                    case ZipArchiveMode.Create:
 0148                        _readEntries = true;
 0149                        break;
 150                    case ZipArchiveMode.Read:
 1738151                        ReadEndOfCentralDirectory();
 1605152                        break;
 153                    case ZipArchiveMode.Update:
 154                    default:
 0155                        Debug.Assert(mode == ZipArchiveMode.Update);
 0156                        if (_archiveStream.Length == 0)
 0157                        {
 0158                            _readEntries = true;
 0159                        }
 160                        else
 0161                        {
 0162                            ReadEndOfCentralDirectory();
 0163                            EnsureCentralDirectoryRead();
 164
 0165                            foreach (ZipArchiveEntry entry in _entries)
 0166                            {
 0167                                entry.ThrowIfNotOpenable(needToUncompress: false, needToLoadIntoMemory: true);
 0168                            }
 0169                        }
 0170                        break;
 171                }
 1605172            }
 133173            catch (Exception)
 133174            {
 133175                extraTempStream?.Dispose();
 176
 133177                throw;
 178            }
 1605179        }
 180
 181        /// Helper constructor that initializes some of the essential ZipArchive
 182        /// information that other constructors initialize the same way.
 183        /// Validations, checks and entry collection need to be done outside this constructor.
 3476184        private ZipArchive(ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding, Stream? backingStream, Stre
 3476185        {
 3476186            _backingStream = backingStream;
 3476187            _archiveStream = archiveStream;
 3476188            _mode = mode;
 3476189            EntryNameAndCommentEncoding = entryNameEncoding;
 3476190            _archiveStreamOwner = null;
 3476191            _entries = new List<ZipArchiveEntry>();
 3476192            _entriesCollection = new ReadOnlyCollection<ZipArchiveEntry>(_entries);
 3476193            _entriesDictionary = new Dictionary<string, ZipArchiveEntry>();
 3476194            Changed = ChangeState.Unchanged;
 3476195            _readEntries = false;
 3476196            _leaveOpen = leaveOpen;
 3476197            _centralDirectoryStart = 0; // invalid until ReadCentralDirectory
 3476198            _isDisposed = false;
 3476199            _numberOfThisDisk = 0; // invalid until ReadCentralDirectory
 3476200            _archiveComment = Array.Empty<byte>();
 3476201            _firstDeletedEntryOffset = long.MaxValue;
 3476202        }
 203
 204        /// <summary>
 205        /// Gets or sets the optional archive comment.
 206        /// </summary>
 207        /// <remarks>
 208        /// The comment encoding is determined by the <c>entryNameEncoding</c> parameter of the <see cref="ZipArchive(St
 209        /// If the comment byte length is larger than <see cref="ushort.MaxValue"/>, it will be truncated when disposing
 210        /// </remarks>
 211        [AllowNull]
 212        public string Comment
 213        {
 0214            get => (EntryNameAndCommentEncoding ?? Encoding.UTF8).GetString(_archiveComment);
 215            set
 0216            {
 0217                _archiveComment = ZipHelper.GetEncodedTruncatedBytesFromString(value, EntryNameAndCommentEncoding, ZipEn
 0218                Changed |= ChangeState.DynamicLengthMetadata;
 0219            }
 220        }
 221
 222        /// <summary>
 223        /// The collection of entries that are currently in the ZipArchive. This may not accurately represent the actual
 224        /// </summary>
 225        /// <exception cref="NotSupportedException">The ZipArchive does not support reading.</exception>
 226        /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception>
 227        /// <exception cref="InvalidDataException">The Zip archive is corrupt and the entries cannot be retrieved.</exce
 228        public ReadOnlyCollection<ZipArchiveEntry> Entries
 229        {
 230            get
 1647231            {
 1647232                if (_mode == ZipArchiveMode.Create)
 0233                {
 0234                    throw new NotSupportedException(SR.EntriesInCreateMode);
 235                }
 236
 1647237                ThrowIfDisposed();
 238
 1647239                EnsureCentralDirectoryRead();
 84240                return _entriesCollection;
 84241            }
 242        }
 243
 244        /// <summary>
 245        /// The ZipArchiveMode that the ZipArchive was initialized with.
 246        /// </summary>
 247        public ZipArchiveMode Mode
 248        {
 249            get
 3294250            {
 3294251                return _mode;
 3294252            }
 253        }
 254
 255        /// <summary>
 256        /// Creates an empty entry in the Zip archive with the specified entry name.
 257        /// There are no restrictions on the names of entries.
 258        /// The last write time of the entry is set to the current time.
 259        /// If an entry with the specified name already exists in the archive, a second entry will be created that has a
 260        /// Since no <code>CompressionLevel</code> is specified, the default provided by the implementation of the under
 261        /// algorithm will be used; the <code>ZipArchive</code> will not impose its own default.
 262        /// (Currently, the underlying compression algorithm is provided by the <code>System.IO.Compression.DeflateStrea
 263        /// </summary>
 264        /// <exception cref="ArgumentException">entryName is a zero-length string.</exception>
 265        /// <exception cref="ArgumentNullException">entryName is null.</exception>
 266        /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception>
 267        /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception>
 268        /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c
 269        /// <returns>A wrapper for the newly created file entry in the archive.</returns>
 270        public ZipArchiveEntry CreateEntry(string entryName)
 0271        {
 0272            return DoCreateEntry(entryName, null);
 0273        }
 274
 275        /// <summary>
 276        /// Creates an empty entry in the Zip archive with the specified entry name. There are no restrictions on the na
 277        /// </summary>
 278        /// <exception cref="ArgumentException">entryName is a zero-length string.</exception>
 279        /// <exception cref="ArgumentNullException">entryName is null.</exception>
 280        /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception>
 281        /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception>
 282        /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c
 283        /// <param name="compressionLevel">The level of the compression (speed/memory vs. compressed size trade-off).</p
 284        /// <returns>A wrapper for the newly created file entry in the archive.</returns>
 285        public ZipArchiveEntry CreateEntry(string entryName, CompressionLevel compressionLevel)
 0286        {
 0287            return DoCreateEntry(entryName, compressionLevel);
 0288        }
 289
 290        /// <summary>
 291        /// Creates an empty encrypted entry in the Zip archive with the specified entry name.
 292        /// The encryption key material is derived from the password and stored on the entry
 293        /// so that a subsequent call to <see cref="ZipArchiveEntry.Open()"/> produces an encrypted stream.
 294        /// </summary>
 295        /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c
 296        /// <param name="password">The password to use for encrypting the entry.</param>
 297        /// <param name="encryptionMethod">The encryption method to use.</param>
 298        /// <returns>A wrapper for the newly created file entry in the archive.</returns>
 299        /// <exception cref="ArgumentException"><paramref name="entryName"/> is a zero-length string.</exception>
 300        /// <exception cref="ArgumentNullException"><paramref name="entryName"/> is <see langword="null"/>.</exception>
 301        /// <exception cref="ArgumentException"><paramref name="password"/> is empty.</exception>
 302        /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception>
 303        /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception>
 304        public ZipArchiveEntry CreateEntry(string entryName, ReadOnlySpan<char> password, ZipEncryptionMethod encryption
 0305        {
 0306            ZipArchiveEntry entry = DoCreateEntry(entryName, null);
 0307            entry.PrepareEncryption(password, encryptionMethod);
 308
 0309            return entry;
 0310        }
 311
 312        /// <summary>
 313        /// Creates an empty encrypted entry in the Zip archive with the specified entry name and compression level.
 314        /// The encryption key material is derived from the password and stored on the entry
 315        /// so that a subsequent call to <see cref="ZipArchiveEntry.Open()"/> produces an encrypted stream.
 316        /// </summary>
 317        /// <param name="entryName">A path relative to the root of the archive, indicating the name of the entry to be c
 318        /// <param name="compressionLevel">The level of the compression (speed/memory vs. compressed size trade-off).</p
 319        /// <param name="password">The password to use for encrypting the entry.</param>
 320        /// <param name="encryptionMethod">The encryption method to use.</param>
 321        /// <returns>A wrapper for the newly created file entry in the archive.</returns>
 322        /// <exception cref="ArgumentException"><paramref name="entryName"/> is a zero-length string.</exception>
 323        /// <exception cref="ArgumentNullException"><paramref name="entryName"/> is <see langword="null"/>.</exception>
 324        /// <exception cref="ArgumentException"><paramref name="password"/> is empty.</exception>
 325        /// <exception cref="NotSupportedException">The ZipArchive does not support writing.</exception>
 326        /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception>
 327        public ZipArchiveEntry CreateEntry(string entryName, CompressionLevel compressionLevel, ReadOnlySpan<char> passw
 0328        {
 0329            ZipArchiveEntry entry = DoCreateEntry(entryName, compressionLevel);
 0330            entry.PrepareEncryption(password, encryptionMethod);
 331
 0332            return entry;
 0333        }
 334
 335        /// <summary>
 336        /// Releases the unmanaged resources used by ZipArchive and optionally finishes writing the archive and releases
 337        /// </summary>
 338        /// <param name="disposing">true to finish writing the archive and release unmanaged and managed resources, fals
 339        protected virtual void Dispose(bool disposing)
 42340        {
 42341            if (disposing && !_isDisposed)
 42342            {
 343                try
 42344                {
 42345                    switch (_mode)
 346                    {
 347                        case ZipArchiveMode.Read:
 42348                            break;
 349                        case ZipArchiveMode.Create:
 0350                            WriteFile();
 0351                            break;
 352                        case ZipArchiveMode.Update:
 353                        default:
 0354                            Debug.Assert(_mode == ZipArchiveMode.Update);
 355                            // Only write if the archive has been modified
 0356                            if (IsModified)
 0357                            {
 0358                                WriteFile();
 0359                            }
 360                            else
 0361                            {
 362                                // Even if we didn't write, unload any entry buffers that may have been loaded
 0363                                foreach (ZipArchiveEntry entry in _entries)
 0364                                {
 0365                                    entry.UnloadStreams();
 0366                                }
 0367                            }
 0368                            break;
 369                    }
 42370                }
 371                finally
 42372                {
 42373                    CloseStreams();
 42374                    _isDisposed = true;
 42375                }
 42376            }
 42377        }
 378        /// <summary>
 379        /// Finishes writing the archive and releases all resources used by the ZipArchive object, unless the object was
 380        /// </summary>
 42381        public void Dispose() => Dispose(true);
 382
 383        /// <summary>
 384        /// Retrieves a wrapper for the file entry in the archive with the specified name. Names are compared using ordi
 385        /// </summary>
 386        /// <exception cref="ArgumentException">entryName is a zero-length string.</exception>
 387        /// <exception cref="ArgumentNullException">entryName is null.</exception>
 388        /// <exception cref="NotSupportedException">The ZipArchive does not support reading.</exception>
 389        /// <exception cref="ObjectDisposedException">The ZipArchive has already been closed.</exception>
 390        /// <exception cref="InvalidDataException">The Zip archive is corrupt and the entries cannot be retrieved.</exce
 391        /// <param name="entryName">A path relative to the root of the archive, identifying the desired entry.</param>
 392        /// <returns>A wrapper for the file entry in the archive. If no entry in the archive exists with the specified n
 393        public ZipArchiveEntry? GetEntry(string entryName)
 0394        {
 0395            ArgumentNullException.ThrowIfNull(entryName);
 396
 0397            if (_mode == ZipArchiveMode.Create)
 0398            {
 0399                throw new NotSupportedException(SR.EntriesInCreateMode);
 400            }
 401
 0402            EnsureCentralDirectoryRead();
 0403            _entriesDictionary.TryGetValue(entryName, out ZipArchiveEntry? result);
 0404            return result;
 0405        }
 406
 4420407        internal Stream ArchiveStream => _archiveStream;
 408
 0409        internal uint NumberOfThisDisk => _numberOfThisDisk;
 410
 411        internal Encoding? EntryNameAndCommentEncoding
 412        {
 29368413            get => _entryNameAndCommentEncoding;
 414
 415            private set
 3476416            {
 417                // value == null is fine. This means the user does not want to overwrite default encoding picking logic.
 418
 419                // The Zip file spec [http://www.pkware.com/documents/casestudies/APPNOTE.TXT] specifies a bit in the en
 420                // (specifically: the language encoding flag (EFS) in the general purpose bit flag of the local file hea
 421                // basically says: UTF8 (1) or CP437 (0). But in reality, tools replace CP437 with "something else that 
 422                // For instance, the Windows Shell Zip tool takes "something else" to mean "the local system codepage".
 423                // We default to the same behaviour, but we let the user explicitly specify the encoding to use for case
 424                // understand their use case well enough.
 425                // Since the definition of acceptable encodings for the "something else" case is in reality by conventio
 426                // immediately clear, whether non-UTF8 Unicode encodings are acceptable. To determine that we would need
 427                // what is currently being done in the field, but we do not have the time for it right now.
 428                // So, we artificially disallow non-UTF8 Unicode encodings for now to make sure we are not creating a co
 429                // for something other tools do not support. If we realise in future that "something else" should includ
 430                // Unicode encodings, we can remove this restriction.
 431
 3476432                if (value != null &&
 3476433                        (value.Equals(Encoding.BigEndianUnicode)
 3476434                        || value.Equals(Encoding.Unicode)))
 0435                {
 0436                    throw new ArgumentException(SR.EntryNameAndCommentEncodingNotSupported, nameof(EntryNameAndCommentEn
 437                }
 438
 3476439                _entryNameAndCommentEncoding = value;
 3476440            }
 441        }
 442
 443        // This property's value only relates to the top-level fields of the archive (such as the archive comment.)
 444        // New entries in the archive won't change its state.
 3476445        internal ChangeState Changed { get; private set; }
 446
 447        /// <summary>
 448        /// Determines whether the archive has been modified and needs to be written.
 449        /// </summary>
 450        private bool IsModified
 451        {
 452            get
 0453            {
 454                // A new archive (created on empty stream) always needs to write the structure
 0455                if (_archiveStream.Length == 0)
 0456                {
 0457                    return true;
 458                }
 459                // Archive-level changes (e.g., comment)
 0460                if (Changed != ChangeState.Unchanged)
 0461                {
 0462                    return true;
 463                }
 464                // Any deleted entries
 0465                if (_firstDeletedEntryOffset != long.MaxValue)
 0466                {
 0467                    return true;
 468                }
 469                // Check if any entry was modified or added
 0470                foreach (ZipArchiveEntry entry in _entries)
 0471                {
 0472                    if (!entry.OriginallyInArchive || entry.Changes != ChangeState.Unchanged)
 0473                    {
 0474                        return true;
 475                    }
 0476                }
 477
 0478                return false;
 0479            }
 480        }
 481
 482        private ZipArchiveEntry DoCreateEntry(string entryName, CompressionLevel? compressionLevel)
 0483        {
 0484            ArgumentException.ThrowIfNullOrEmpty(entryName);
 485
 0486            if (_mode == ZipArchiveMode.Read)
 0487            {
 0488                throw new NotSupportedException(SR.CreateInReadMode);
 489            }
 490
 0491            ThrowIfDisposed();
 492
 493
 0494            ZipArchiveEntry entry = compressionLevel.HasValue ?
 0495                new ZipArchiveEntry(this, entryName, compressionLevel.Value) :
 0496                new ZipArchiveEntry(this, entryName);
 497
 0498            AddEntry(entry);
 499
 0500            return entry;
 0501        }
 502
 503        internal void AcquireArchiveStream(ZipArchiveEntry entry)
 0504        {
 505            // if a previous entry had held the stream but never wrote anything, we write their local header for them
 0506            if (_archiveStreamOwner != null)
 0507            {
 0508                if (!_archiveStreamOwner.EverOpenedForWrite)
 0509                {
 0510                    _archiveStreamOwner.WriteAndFinishLocalEntry(forceWrite: true);
 0511                }
 512                else
 0513                {
 0514                    throw new IOException(SR.CreateModeCreateEntryWhileOpen);
 515                }
 0516            }
 517
 0518            _archiveStreamOwner = entry;
 0519        }
 520
 521        private void AddEntry(ZipArchiveEntry entry)
 29756522        {
 29756523            _entries.Add(entry);
 29756524            _entriesDictionary.TryAdd(entry.FullName, entry);
 29756525        }
 526
 527        [Conditional("DEBUG")]
 0528        internal void DebugAssertIsStillArchiveStreamOwner(ZipArchiveEntry entry) => Debug.Assert(_archiveStreamOwner ==
 529
 530        internal void ReleaseArchiveStream(ZipArchiveEntry entry)
 0531        {
 0532            Debug.Assert(_archiveStreamOwner == entry);
 533
 0534            _archiveStreamOwner = null;
 0535        }
 536
 537        internal void RemoveEntry(ZipArchiveEntry entry)
 0538        {
 0539            _entries.Remove(entry);
 540
 0541            _entriesDictionary.Remove(entry.FullName);
 542            // Keep track of the offset of the earliest deleted entry in the archive
 0543            if (entry.OriginallyInArchive && entry.OffsetOfLocalHeader < _firstDeletedEntryOffset)
 0544            {
 0545                _firstDeletedEntryOffset = entry.OffsetOfLocalHeader;
 0546            }
 0547        }
 548
 549        internal void ThrowIfDisposed()
 1647550        {
 1647551            ObjectDisposedException.ThrowIf(_isDisposed, this);
 1647552        }
 553
 554        private void CloseStreams()
 42555        {
 42556            if (!_leaveOpen)
 42557            {
 42558                _archiveStream.Dispose();
 42559                _backingStream?.Dispose();
 42560            }
 561            else
 0562            {
 563                // if _backingStream isn't null, that means we assigned the original stream they passed
 564                // us to _backingStream (which they requested we leave open), and _archiveStream was
 565                // the temporary copy that we needed
 0566                if (_backingStream != null)
 0567                {
 0568                    _archiveStream.Dispose();
 0569                }
 0570            }
 42571        }
 572
 573        private void EnsureCentralDirectoryRead()
 1647574        {
 1647575            if (!_readEntries)
 1605576            {
 1605577                ReadCentralDirectory();
 42578                _readEntries = true;
 42579            }
 84580        }
 581
 582        private void ReadCentralDirectoryInitialize(out long numberOfEntries, out bool saveExtraFieldsAndComments, out b
 3210583        {
 584            // assume ReadEndOfCentralDirectory has been called and has populated _centralDirectoryStart
 585
 3210586            _archiveStream.Seek(_centralDirectoryStart, SeekOrigin.Begin);
 587
 3210588            numberOfEntries = 0;
 3210589            saveExtraFieldsAndComments = Mode == ZipArchiveMode.Update;
 590
 3210591            continueReadingCentralDirectory = true;
 592            // total bytes read from central directory
 3210593            bytesRead = 0;
 594            // current position in the current buffer
 3210595            currPosition = 0;
 596            // total bytes read from all file headers starting in the current buffer
 3210597            bytesConsumed = 0;
 598
 3210599            _entries.Clear();
 3210600            _entriesDictionary.Clear();
 3210601        }
 602
 603        private bool ReadCentralDirectoryEndOfInnerLoopWork(bool result, ZipCentralDirectoryFileHeader? currentHeader, i
 30986604        {
 30986605            if (!result)
 1230606            {
 1230607                continueReadingCentralDirectory = false;
 1230608                return false;
 609            }
 610
 29756611            Debug.Assert(currentHeader != null, "currentHeader should not be null here");
 29756612            AddEntry(new ZipArchiveEntry(this, currentHeader));
 29756613            numberOfEntries++;
 29756614            if (numberOfEntries > _expectedNumberOfEntries)
 560615            {
 560616                throw new InvalidDataException(SR.NumEntriesWrong);
 617            }
 618
 29196619            currPosition += bytesConsumed;
 29196620            bytesRead += bytesConsumed;
 621
 29196622            return true;
 30426623        }
 624
 625        private void ReadCentralDirectoryEndOfOuterLoopWork(ref int currPosition, ReadOnlySpan<byte> sizedFileBuffer)
 3792626        {
 627            // We've run out of possible space in the entry - seek backwards by the number of bytes remaining in
 628            // this buffer (so that the next buffer overlaps with this one) and retry.
 3792629            if (currPosition < sizedFileBuffer.Length)
 3600630            {
 3600631                _archiveStream.Seek(-(sizedFileBuffer.Length - currPosition), SeekOrigin.Current);
 3600632            }
 3792633            currPosition = 0;
 3792634        }
 635
 636        private void ReadCentralDirectoryPostOuterLoopWork(long numberOfEntries)
 2524637        {
 2524638            if (numberOfEntries != _expectedNumberOfEntries)
 2440639            {
 2440640                throw new InvalidDataException(SR.NumEntriesWrong);
 641            }
 642
 643            // Sort _entries by each archive entry's position. This supports the algorithm in WriteFile, so is only
 644            // necessary when the ZipArchive has been opened in Update mode.
 84645            if (Mode == ZipArchiveMode.Update)
 0646            {
 0647                _entries.Sort(ZipArchiveEntry.LocalHeaderOffsetComparer.Instance);
 648
 649                // Precompute EndOfLocalEntryData for each entry. At read time every entry is original
 650                // and sorted by offset, so entry[i]'s end is entry[i+1]'s start (or the central directory
 651                // for the last entry). This correctly includes any trailing data descriptor bytes.
 0652                for (int i = 0; i < _entries.Count; i++)
 0653                {
 0654                    _entries[i].EndOfLocalEntryData = i < _entries.Count - 1
 0655                        ? _entries[i + 1].OffsetOfLocalHeader
 0656                        : _centralDirectoryStart;
 0657                }
 0658            }
 84659        }
 660
 661        private void ReadCentralDirectory()
 1605662        {
 1605663            byte[] arrayPoolArray = ArrayPool<byte>.Shared.Rent(ReadCentralDirectoryReadBufferSize);
 664            try
 1605665            {
 1605666                Span<byte> fileBuffer = arrayPoolArray.AsSpan();
 667
 1605668                ReadCentralDirectoryInitialize(out long numberOfEntries, out bool saveExtraFieldsAndComments, out bool c
 669
 670                // read the central directory
 3501671                while (continueReadingCentralDirectory)
 2239672                {
 673                    // the buffer read must always be large enough to fit the constant section size of at least one head
 2239674                    int currBytesRead = _archiveStream.ReadAtLeast(fileBuffer, ZipCentralDirectoryFileHeader.BlockConsta
 675
 2239676                    ReadOnlySpan<byte> sizedFileBuffer = fileBuffer.Slice(0, currBytesRead);
 2239677                    continueReadingCentralDirectory = currBytesRead >= ZipCentralDirectoryFileHeader.BlockConstantSectio
 678
 16837679                    while (currPosition + ZipCentralDirectoryFileHeader.BlockConstantSectionSize <= currBytesRead)
 15556680                    {
 15556681                        bool result = ZipCentralDirectoryFileHeader.TryReadBlock(sizedFileBuffer.Slice(currPosition), _a
 15556682                            saveExtraFieldsAndComments, out bytesConsumed, out ZipCentralDirectoryFileHeader? currentHea
 683
 15493684                        if (!ReadCentralDirectoryEndOfInnerLoopWork(result, currentHeader, bytesConsumed, ref continueRe
 615685                        {
 615686                            break;
 687                        }
 688
 14598689                        ZipArchiveEntry lastEntry = _entries[_entries.Count - 1];
 14598690                        if (lastEntry.IsEncrypted)
 903691                        {
 903692                            lastEntry.ReadEncryptionSaltIfNeeded();
 903693                        }
 14598694                    }
 695
 1896696                    ReadCentralDirectoryEndOfOuterLoopWork(ref currPosition, sizedFileBuffer);
 1896697                }
 698
 1262699                ReadCentralDirectoryPostOuterLoopWork(numberOfEntries);
 42700            }
 0701            catch (EndOfStreamException ex)
 0702            {
 0703                throw new InvalidDataException(SR.CentralDirectoryInvalid, ex);
 704            }
 705            finally
 1605706            {
 1605707                ArrayPool<byte>.Shared.Return(arrayPoolArray);
 1605708            }
 42709        }
 710
 711        private void ReadEndOfCentralDirectoryInnerWork(ZipEndOfCentralDirectoryBlock eocd)
 3446712        {
 3446713            if (eocd.NumberOfThisDisk != eocd.NumberOfTheDiskWithTheStartOfTheCentralDirectory)
 10714            {
 10715                throw new InvalidDataException(SR.SplitSpanned);
 716            }
 717
 3436718            _numberOfThisDisk = eocd.NumberOfThisDisk;
 3436719            _centralDirectoryStart = eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber;
 720
 3436721            if (eocd.NumberOfEntriesInTheCentralDirectory != eocd.NumberOfEntriesInTheCentralDirectoryOnThisDisk)
 6722            {
 6723                throw new InvalidDataException(SR.SplitSpanned);
 724            }
 725
 3430726            _expectedNumberOfEntries = eocd.NumberOfEntriesInTheCentralDirectory;
 727
 3430728            _archiveComment = eocd.ArchiveComment;
 3430729        }
 730
 731        // This function reads all the EOCD stuff it needs to find the offset to the start of the central directory
 732        // This offset gets put in _centralDirectoryStart and the number of this disk gets put in _numberOfThisDisk
 733        // Also does some verification that this isn't a split/spanned archive
 734        // Also checks that offset to CD isn't out of bounds
 735        private void ReadEndOfCentralDirectory()
 1738736        {
 737            try
 1738738            {
 739                // This seeks backwards almost to the beginning of the EOCD, one byte after where the signature would be
 740                // located if the EOCD had the minimum possible size (no file zip comment)
 1738741                _archiveStream.Seek(-ZipEndOfCentralDirectoryBlock.SizeOfBlockWithoutSignature, SeekOrigin.End);
 742
 743                // If the EOCD has the minimum possible size (no zip file comment), then exactly the previous 4 bytes wi
 744                // But if the EOCD has max possible size, the signature should be found somewhere in the previous 64K + 
 1735745                if (!ZipHelper.SeekBackwardsToSignature(_archiveStream,
 1735746                        ZipEndOfCentralDirectoryBlock.SignatureConstantBytes,
 1735747                        ZipEndOfCentralDirectoryBlock.ZipFileCommentMaxLength + ZipEndOfCentralDirectoryBlock.FieldLengt
 6748                    throw new InvalidDataException(SR.EOCDNotFound);
 749
 1729750                long eocdStart = _archiveStream.Position;
 751
 752                // read the EOCD
 1729753                ZipEndOfCentralDirectoryBlock eocd = ZipEndOfCentralDirectoryBlock.ReadBlock(_archiveStream);
 754
 1723755                ReadEndOfCentralDirectoryInnerWork(eocd);
 756
 1715757                TryReadZip64EndOfCentralDirectory(eocd, eocdStart);
 758
 1614759                if (_centralDirectoryStart > _archiveStream.Length)
 9760                {
 9761                    throw new InvalidDataException(SR.FieldTooBigOffsetToCD);
 762                }
 1605763            }
 0764            catch (EndOfStreamException ex)
 0765            {
 0766                throw new InvalidDataException(SR.CDCorrupt, ex);
 767            }
 3768            catch (IOException ex)
 3769            {
 3770                throw new InvalidDataException(SR.CDCorrupt, ex);
 771            }
 1605772        }
 773
 774        private void TryReadZip64EndOfCentralDirectoryInnerInitialWork(Zip64EndOfCentralDirectoryLocator? locator)
 178775        {
 178776            if (locator == null || locator.OffsetOfZip64EOCD > long.MaxValue)
 64777            {
 64778                throw new InvalidDataException(SR.FieldTooBigOffsetToZip64EOCD);
 779            }
 780
 114781            long zip64EOCDOffset = (long)locator.OffsetOfZip64EOCD;
 782
 114783            if (zip64EOCDOffset < 0 || zip64EOCDOffset > _archiveStream.Length)
 60784            {
 60785                throw new InvalidDataException(SR.InvalidOffsetToZip64EOCD);
 786            }
 787
 54788            _archiveStream.Seek(zip64EOCDOffset, SeekOrigin.Begin);
 54789        }
 790
 791        private void TryReadZip64EndOfCentralDirectoryInnerFinalWork(Zip64EndOfCentralDirectoryRecord record)
 36792        {
 36793            _numberOfThisDisk = record.NumberOfThisDisk;
 794
 36795            if (record.NumberOfEntriesTotal > long.MaxValue)
 18796            {
 18797                throw new InvalidDataException(SR.FieldTooBigNumEntries);
 798            }
 799
 18800            if (record.OffsetOfCentralDirectory > long.MaxValue)
 14801            {
 14802                throw new InvalidDataException(SR.FieldTooBigOffsetToCD);
 803            }
 804
 4805            if (record.NumberOfEntriesTotal != record.NumberOfEntriesOnThisDisk)
 4806            {
 4807                throw new InvalidDataException(SR.SplitSpanned);
 808            }
 809
 0810            _expectedNumberOfEntries = (long)record.NumberOfEntriesTotal;
 0811            _centralDirectoryStart = (long)record.OffsetOfCentralDirectory;
 0812        }
 813
 814        // Tries to find the Zip64 End of Central Directory Locator, then the Zip64 End of Central Directory, assuming t
 815        // End of Central Directory block has already been found, as well as the location in the stream where the EOCD s
 816        private void TryReadZip64EndOfCentralDirectory(ZipEndOfCentralDirectoryBlock eocd, long eocdStart)
 1715817        {
 818            // Only bother looking for the Zip64-EOCD stuff if we suspect it is needed because some value is FFFFFFFFF
 819            // because these are the only two values we need, we only worry about these
 820            // if we don't find the Zip64-EOCD, we just give up and try to use the original values
 1715821            if (eocd.NumberOfThisDisk == ZipHelper.Mask16Bit ||
 1715822                eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber == ZipHelper.Mask32Bit ||
 1715823                eocd.NumberOfEntriesInTheCentralDirectory == ZipHelper.Mask16Bit)
 1292824            {
 825                // Read Zip64 End of Central Directory Locator
 826
 827                // Check if there's enough space before the EOCD to look for the Zip64 EOCDL
 1292828                if (eocdStart < Zip64EndOfCentralDirectoryLocator.TotalSize)
 12829                {
 12830                    throw new InvalidDataException(SR.Zip64EOCDNotWhereExpected);
 831                }
 832
 833                // This seeks forwards almost to the beginning of the Zip64-EOCDL, one byte after where the signature wo
 1280834                _archiveStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigi
 835
 836                // Exactly the previous 4 bytes should contain the Zip64-EOCDL signature
 837                // if we don't find it, assume it doesn't exist and use data from normal EOCD
 1280838                if (ZipHelper.SeekBackwardsToSignature(_archiveStream,
 1280839                        Zip64EndOfCentralDirectoryLocator.SignatureConstantBytes,
 1280840                        Zip64EndOfCentralDirectoryLocator.FieldLengths.Signature))
 89841                {
 842                    // use locator to get to Zip64-EOCD
 89843                    Zip64EndOfCentralDirectoryLocator locator = Zip64EndOfCentralDirectoryLocator.TryReadBlock(_archiveS
 89844                    TryReadZip64EndOfCentralDirectoryInnerInitialWork(locator);
 845
 846                    // Read Zip64 End of Central Directory Record
 27847                    Zip64EndOfCentralDirectoryRecord record = Zip64EndOfCentralDirectoryRecord.TryReadBlock(_archiveStre
 18848                    TryReadZip64EndOfCentralDirectoryInnerFinalWork(record);
 0849                }
 1191850            }
 1614851        }
 852
 853        private static void WriteFileCalculateOffsets(ZipArchiveEntry entry, ref long startingOffset, ref long nextFileO
 0854        {
 0855            if (entry.Changes == ChangeState.Unchanged)
 0856            {
 857                // Keep track of the expected position of the file entry after the final untouched file entry so that wh
 858                // we'll know which position to start writing new entries from.
 859                // EndOfLocalEntryData includes any trailing data descriptor because it was pre-computed from the
 860                // next entry's offset or the central directory start.
 0861                nextFileOffset = Math.Max(nextFileOffset, entry.EndOfLocalEntryData);
 0862            }
 863            // When calculating the starting offset to load the files from, only look at changed entries which are alrea
 864            else
 0865            {
 0866                startingOffset = Math.Min(startingOffset, entry.OffsetOfLocalHeader);
 0867            }
 0868        }
 869
 870        private static void WriteFileCheckStartingOffset(ZipArchiveEntry entry, ref long completeRewriteStartingOffset)
 0871        {
 872            // If the pending data to write is fixed-length metadata in the header, there's no need to load the compress
 873            // We always need to load the local file header's metadata though - at this point, this entry will be writte
 874            // want to make sure that we preserve that metadata.
 0875            if ((entry.Changes & (ChangeState.DynamicLengthMetadata | ChangeState.StoredData)) != 0)
 0876            {
 0877                completeRewriteStartingOffset = Math.Min(completeRewriteStartingOffset, entry.OffsetOfLocalHeader);
 0878            }
 0879        }
 880
 881        private void WriteFileUpdateModeFinalWork(long startingOffset, long nextFileOffset)
 0882        {
 883            // If the offset of entries to write from is still at long.MaxValue, then we know that nothing has been dele
 884            // nothing has been modified - so we just want to move to the end of all remaining files in the archive.
 0885            if (startingOffset == long.MaxValue)
 0886            {
 0887                startingOffset = nextFileOffset;
 0888            }
 889
 0890            _archiveStream.Seek(startingOffset, SeekOrigin.Begin);
 0891        }
 892
 893        private void WriteFileFinalWork()
 0894        {
 895            // If entries have been removed and new (smaller) ones added, there could be empty space at the end of the f
 896            // Shrink the file to reclaim this space.
 0897            if (_mode == ZipArchiveMode.Update && _archiveStream.Position != _archiveStream.Length)
 0898            {
 0899                _archiveStream.SetLength(_archiveStream.Position);
 0900            }
 0901        }
 902
 903        private void WriteFile()
 0904        {
 905            // if we are in create mode, we always set readEntries to true in Init
 906            // if we are in update mode, we call EnsureCentralDirectoryRead, which sets readEntries to true
 0907            Debug.Assert(_readEntries);
 908
 909            // Entries starting after this offset have had a dynamically-sized change. Everything on or after this point
 0910            long completeRewriteStartingOffset = 0;
 0911            List<ZipArchiveEntry> entriesToWrite = _entries;
 912
 0913            if (_mode == ZipArchiveMode.Update)
 0914            {
 915                // Entries starting after this offset have some kind of change made to them. It might just be a fixed-le
 916                // that single entry's metadata can be rewritten without impacting anything else.
 0917                long startingOffset = _firstDeletedEntryOffset;
 0918                long nextFileOffset = 0;
 0919                completeRewriteStartingOffset = startingOffset;
 0920                entriesToWrite = new(_entries.Count);
 921
 0922                foreach (ZipArchiveEntry entry in _entries)
 0923                {
 0924                    if (!entry.OriginallyInArchive)
 0925                    {
 0926                        entriesToWrite.Add(entry);
 0927                    }
 928                    else
 0929                    {
 0930                        WriteFileCalculateOffsets(entry, ref startingOffset, ref nextFileOffset);
 931
 932                        // We want to re-write entries which are after the starting offset of the first entry which has 
 933                        // NB: the existing ZipArchiveEntries are sorted in _entries by their position ascending.
 0934                        if (entry.OffsetOfLocalHeader >= startingOffset)
 0935                        {
 0936                            WriteFileCheckStartingOffset(entry, ref completeRewriteStartingOffset);
 937
 0938                            entry.LoadLocalHeaderExtraFieldIfNeeded();
 0939                            if (entry.OffsetOfLocalHeader >= completeRewriteStartingOffset)
 0940                            {
 0941                                entry.LoadCompressedBytesIfNeeded();
 0942                            }
 943
 0944                            entriesToWrite.Add(entry);
 0945                        }
 0946                    }
 0947                }
 0948                WriteFileUpdateModeFinalWork(startingOffset, nextFileOffset);
 0949            }
 950
 0951            foreach (ZipArchiveEntry entry in entriesToWrite)
 0952            {
 953                // We don't always need to write the local header entry, ZipArchiveEntry is usually able to work out whe
 954                // We want to force this header entry to be written (even for completely untouched entries) if the entry
 955                // which had a pending dynamically-sized write.
 0956                bool forceWriteLocalEntry = !entry.OriginallyInArchive || (entry.OriginallyInArchive && entry.OffsetOfLo
 957
 0958                entry.WriteAndFinishLocalEntry(forceWriteLocalEntry);
 0959            }
 960
 0961            long plannedCentralDirectoryPosition = _archiveStream.Position;
 962            // If there are no entries in the archive, we still want to create the archive epilogue.
 0963            bool archiveEpilogueRequiresUpdate = _entries.Count == 0;
 964
 0965            foreach (ZipArchiveEntry entry in _entries)
 0966            {
 967                // The central directory needs to be rewritten if its position has moved, if there's a new entry in the 
 0968                bool centralDirectoryEntryRequiresUpdate = plannedCentralDirectoryPosition != _centralDirectoryStart
 0969                    || !entry.OriginallyInArchive || entry.OffsetOfLocalHeader >= completeRewriteStartingOffset;
 970
 0971                entry.WriteCentralDirectoryFileHeader(centralDirectoryEntryRequiresUpdate);
 0972                archiveEpilogueRequiresUpdate |= centralDirectoryEntryRequiresUpdate;
 0973            }
 974
 0975            long sizeOfCentralDirectory = _archiveStream.Position - plannedCentralDirectoryPosition;
 976
 0977            WriteArchiveEpilogue(plannedCentralDirectoryPosition, sizeOfCentralDirectory, archiveEpilogueRequiresUpdate)
 978
 0979            WriteFileFinalWork();
 0980        }
 981
 982        private void WriteArchiveEpilogueNoCDChangesWork()
 0983        {
 0984            _archiveStream.Seek(Zip64EndOfCentralDirectoryRecord.TotalSize, SeekOrigin.Current);
 0985            _archiveStream.Seek(Zip64EndOfCentralDirectoryLocator.TotalSize, SeekOrigin.Current);
 0986        }
 987
 988        // writes eocd, and if needed, zip 64 eocd, zip64 eocd locator
 989        // should only throw an exception in extremely exceptional cases because it is called from dispose
 990        private void WriteArchiveEpilogue(long startOfCentralDirectory, long sizeOfCentralDirectory, bool centralDirecto
 0991        {
 992            // determine if we need Zip 64
 0993            if (startOfCentralDirectory >= uint.MaxValue
 0994                || sizeOfCentralDirectory >= uint.MaxValue
 0995                || _entries.Count >= ZipHelper.Mask16Bit
 0996#if DEBUG_FORCE_ZIP64
 0997                || _forceZip64
 0998#endif
 0999                )
 01000            {
 1001                // if we need zip 64, write zip 64 eocd and locator
 01002                long zip64EOCDRecordStart = _archiveStream.Position;
 1003
 01004                if (centralDirectoryChanged)
 01005                {
 01006                    Zip64EndOfCentralDirectoryRecord.WriteBlock(_archiveStream, _entries.Count, startOfCentralDirectory,
 01007                    Zip64EndOfCentralDirectoryLocator.WriteBlock(_archiveStream, zip64EOCDRecordStart);
 01008                }
 1009                else
 01010                {
 01011                    WriteArchiveEpilogueNoCDChangesWork();
 01012                }
 01013            }
 1014
 1015            // write normal eocd
 01016            if (centralDirectoryChanged || (Changed != ChangeState.Unchanged))
 01017            {
 01018                ZipEndOfCentralDirectoryBlock.WriteBlock(_archiveStream, _entries.Count, startOfCentralDirectory, sizeOf
 01019            }
 1020            else
 01021            {
 01022                _archiveStream.Seek(ZipEndOfCentralDirectoryBlock.TotalSize + _archiveComment.Length, SeekOrigin.Current
 01023            }
 01024        }
 1025
 1026        // Confirms that the specified stream is compatible with the specified mode.
 1027        // Returns a boolean that indicates that further work needs to be done for when
 1028        // the mode is Read and the stream is unseekable.
 1029        private static bool ValidateMode(ZipArchiveMode mode, Stream stream)
 34761030        {
 1031            // check stream against mode
 34761032            bool isReadModeAndUnseekable = false;
 1033
 34761034            switch (mode)
 1035            {
 1036                case ZipArchiveMode.Create:
 01037                    if (!stream.CanWrite)
 01038                    {
 01039                        throw new ArgumentException(SR.CreateModeCapabilities);
 1040                    }
 01041                    break;
 1042                case ZipArchiveMode.Read:
 34761043                    if (!stream.CanRead)
 01044                    {
 01045                        throw new ArgumentException(SR.ReadModeCapabilities);
 1046                    }
 34761047                    if (!stream.CanSeek)
 01048                    {
 01049                        isReadModeAndUnseekable = true;
 01050                    }
 34761051                    break;
 1052                case ZipArchiveMode.Update:
 01053                    if (!stream.CanRead || !stream.CanWrite || !stream.CanSeek)
 01054                    {
 01055                        throw new ArgumentException(SR.UpdateModeCapabilities);
 1056                    }
 01057                    break;
 1058                default:
 1059                    // still have to throw this, because stream constructor doesn't do mode argument checks
 01060                    throw new ArgumentOutOfRangeException(nameof(mode));
 1061            }
 1062
 34761063            return isReadModeAndUnseekable;
 34761064        }
 1065
 1066        // Depending on mode and stream seekability, we will decide if the archive
 1067        // stream needs to be wrapped or not by another stream to help with writing.
 1068        private static Stream DecideArchiveStream(ZipArchiveMode mode, Stream stream)
 52141069        {
 52141070            ArgumentNullException.ThrowIfNull(stream);
 1071
 52141072            return mode == ZipArchiveMode.Create && !stream.CanSeek ?
 52141073                new PositionPreservingWriteOnlyStreamWrapper(stream) :
 52141074                stream;
 52141075        }
 1076
 1077
 1078        [Flags]
 1079        internal enum ChangeState
 1080        {
 1081            Unchanged = 0x0,
 1082            FixedLengthMetadata = 0x1,
 1083            DynamicLengthMetadata = 0x2,
 1084            StoredData = 0x4
 1085        }
 1086    }
 1087}

Methods/Properties

CreateAsync(System.IO.Stream,System.IO.Compression.ZipArchiveMode,System.Boolean,System.Text.Encoding,System.Threading.CancellationToken)
DisposeAsync()
DisposeAsyncCore()
CloseStreamsAsync()
EnsureCentralDirectoryReadAsync(System.Threading.CancellationToken)
ReadCentralDirectoryAsync(System.Threading.CancellationToken)
ReadEndOfCentralDirectoryAsync(System.Threading.CancellationToken)
TryReadZip64EndOfCentralDirectoryAsync(System.IO.Compression.ZipEndOfCentralDirectoryBlock,System.Int64,System.Threading.CancellationToken)
WriteFileAsync(System.Threading.CancellationToken)
WriteArchiveEpilogueAsync(System.Int64,System.Int64,System.Boolean,System.Threading.CancellationToken)
.ctor(System.IO.Stream)
.ctor(System.IO.Stream,System.IO.Compression.ZipArchiveMode)
.ctor(System.IO.Stream,System.IO.Compression.ZipArchiveMode,System.Boolean)
.ctor(System.IO.Stream,System.IO.Compression.ZipArchiveMode,System.Boolean,System.Text.Encoding)
.ctor(System.IO.Compression.ZipArchiveMode,System.Boolean,System.Text.Encoding,System.IO.Stream,System.IO.Stream)
Comment()
Comment(System.String)
Entries()
Mode()
CreateEntry(System.String)
CreateEntry(System.String,System.IO.Compression.CompressionLevel)
CreateEntry(System.String,System.ReadOnlySpan`1<System.Char>,System.IO.Compression.ZipEncryptionMethod)
CreateEntry(System.String,System.IO.Compression.CompressionLevel,System.ReadOnlySpan`1<System.Char>,System.IO.Compression.ZipEncryptionMethod)
Dispose(System.Boolean)
Dispose()
GetEntry(System.String)
ArchiveStream()
NumberOfThisDisk()
EntryNameAndCommentEncoding()
EntryNameAndCommentEncoding(System.Text.Encoding)
Changed()
IsModified()
DoCreateEntry(System.String,System.Nullable`1<System.IO.Compression.CompressionLevel>)
AcquireArchiveStream(System.IO.Compression.ZipArchiveEntry)
AddEntry(System.IO.Compression.ZipArchiveEntry)
DebugAssertIsStillArchiveStreamOwner(System.IO.Compression.ZipArchiveEntry)
ReleaseArchiveStream(System.IO.Compression.ZipArchiveEntry)
RemoveEntry(System.IO.Compression.ZipArchiveEntry)
ThrowIfDisposed()
CloseStreams()
EnsureCentralDirectoryRead()
ReadCentralDirectoryInitialize(System.Int64&,System.Boolean&,System.Boolean&,System.Int32&,System.Int32&,System.Int32&)
ReadCentralDirectoryEndOfInnerLoopWork(System.Boolean,System.IO.Compression.ZipCentralDirectoryFileHeader,System.Int32,System.Boolean&,System.Int64&,System.Int32&,System.Int32&)
ReadCentralDirectoryEndOfOuterLoopWork(System.Int32&,System.ReadOnlySpan`1<System.Byte>)
ReadCentralDirectoryPostOuterLoopWork(System.Int64)
ReadCentralDirectory()
ReadEndOfCentralDirectoryInnerWork(System.IO.Compression.ZipEndOfCentralDirectoryBlock)
ReadEndOfCentralDirectory()
TryReadZip64EndOfCentralDirectoryInnerInitialWork(System.IO.Compression.Zip64EndOfCentralDirectoryLocator)
TryReadZip64EndOfCentralDirectoryInnerFinalWork(System.IO.Compression.Zip64EndOfCentralDirectoryRecord)
TryReadZip64EndOfCentralDirectory(System.IO.Compression.ZipEndOfCentralDirectoryBlock,System.Int64)
WriteFileCalculateOffsets(System.IO.Compression.ZipArchiveEntry,System.Int64&,System.Int64&)
WriteFileCheckStartingOffset(System.IO.Compression.ZipArchiveEntry,System.Int64&)
WriteFileUpdateModeFinalWork(System.Int64,System.Int64)
WriteFileFinalWork()
WriteFile()
WriteArchiveEpilogueNoCDChangesWork()
WriteArchiveEpilogue(System.Int64,System.Int64,System.Boolean)
ValidateMode(System.IO.Compression.ZipArchiveMode,System.IO.Stream)
DecideArchiveStream(System.IO.Compression.ZipArchiveMode,System.IO.Stream)