File: System\IO\Packaging\ZipPackage.cs
Web Access
Project: src\src\libraries\System.IO.Packaging\src\System.IO.Packaging.csproj (System.IO.Packaging)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using System.Xml;                   //Required for Content Type File manipulation
 
namespace System.IO.Packaging
{
    /// <summary>
    /// ZipPackage is a specific implementation for the abstract Package
    /// class, corresponding to the Zip file format.
    /// This is a part of the Packaging Layer APIs.
    /// </summary>
    public sealed class ZipPackage : Package
    {
        #region Public Methods
 
        #region PackagePart Methods
 
        /// <summary>
        /// This method is for custom implementation for the underlying file format
        /// Adds a new item to the zip archive corresponding to the PackagePart in the package.
        /// </summary>
        /// <param name="partUri">PartName</param>
        /// <param name="contentType">Content type of the part</param>
        /// <param name="compressionOption">Compression option for this part</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">If partUri parameter is null</exception>
        /// <exception cref="ArgumentNullException">If contentType parameter is null</exception>
        /// <exception cref="ArgumentException">If partUri parameter does not conform to the valid partUri syntax</exception>
        /// <exception cref="ArgumentOutOfRangeException">If CompressionOption enumeration [compressionOption] does not have one of the valid values</exception>
        protected override PackagePart CreatePartCore(Uri partUri,
            string contentType,
            CompressionOption compressionOption)
        {
            //Validating the PartUri - this method will do the argument checking required for uri.
            partUri = PackUriHelper.ValidatePartUri(partUri);
 
            if (contentType == null)
                throw new ArgumentNullException(nameof(contentType));
 
            Package.ThrowIfCompressionOptionInvalid(compressionOption);
 
            // Convert XPS CompressionOption to Zip CompressionMethodEnum.
            CompressionLevel level;
            GetZipCompressionMethodFromOpcCompressionOption(compressionOption,
                out level);
 
            // If any entries are present in the ignoredItemList that might correspond to
            // the same part name, we delete all those entries.
            _ignoredItemHelper.Delete((PackUriHelper.ValidatedPartUri)partUri);
 
            // Create new Zip item.
            // We need to remove the leading "/" character at the beginning of the part name.
            // The partUri object must be a ValidatedPartUri
            string zipItemName = ((PackUriHelper.ValidatedPartUri)partUri).PartUriString.Substring(1);
 
            ZipArchiveEntry zipArchiveEntry = _zipArchive.CreateEntry(zipItemName, level);
 
            //Store the content type of this part in the content types stream.
            _contentTypeHelper.AddContentType((PackUriHelper.ValidatedPartUri)partUri, new ContentType(contentType), level);
 
            return new ZipPackagePart(this, zipArchiveEntry.Archive, zipArchiveEntry, _zipStreamManager, (PackUriHelper.ValidatedPartUri)partUri, contentType, compressionOption);
        }
 
        /// <summary>
        /// This method is for custom implementation specific to the file format.
        /// Returns the part after reading the actual physical bits. The method
        /// returns a null to indicate that the part corresponding to the specified
        /// Uri was not found in the container.
        /// This method does not throw an exception if a part does not exist.
        /// </summary>
        /// <param name="partUri"></param>
        /// <returns></returns>
        protected override PackagePart? GetPartCore(Uri partUri)
        {
            //Currently the design has two aspects which makes it possible to return
            //a null from this method -
            //  1. All the parts are loaded at Package.Open time and as such, this
            //     method would not be invoked, unless the user is asking for -
            //     i. a part that does not exist - we can safely return null
            //     ii.a part(interleaved/non-interleaved) that was added to the
            //        underlying package by some other means, and the user wants to
            //        access the updated part. This is currently not possible as the
            //        underlying zip i/o layer does not allow for FileShare.ReadWrite.
            //  2. Also, its not a straightforward task to determine if a new part was
            //     added as we need to look for atomic as well as interleaved parts and
            //     this has to be done in a case sensitive manner. So, effectively
            //     we will have to go through the entire list of zip items to determine
            //     if there are any updates.
            //  If ever the design changes, then this method must be updated accordingly
 
            return null;
        }
 
        /// <summary>
        /// This method is for custom implementation specific to the file format.
        /// Deletes the part corresponding to the uri specified. Deleting a part that does not
        /// exists is not an error and so we do not throw an exception in that case.
        /// </summary>
        /// <param name="partUri"></param>
        /// <exception cref="ArgumentNullException">If partUri parameter is null</exception>
        /// <exception cref="ArgumentException">If partUri parameter does not conform to the valid partUri syntax</exception>
        protected override void DeletePartCore(Uri partUri)
        {
            //Validating the PartUri - this method will do the argument checking required for uri.
            PackUriHelper.ValidatedPartUri validatedUri = PackUriHelper.ValidatePartUri(partUri);
 
            string partZipName = GetZipItemNameFromOpcName(PackUriHelper.GetStringForPartUri(validatedUri));
            ZipArchiveEntry? zipArchiveEntry = _zipArchive.GetEntry(partZipName);
            if (zipArchiveEntry != null)
            {
                // Case of an atomic part.
                zipArchiveEntry.Delete();
            }
            else
            {
                // This can happen if the part is interleaved.
                // Important Note: This method relies on the fact that the base class does not
                // clean up all the information about the part to be deleted before this method
                // is called. If ever that behaviour in Package.Delete() changes, this method
                // should be changed.
                // Ideally we would have liked to avoid this kind of a restriction but due to the
                // current class interfaces and data structure ownerships between these objects,
                // it's tough to re-design at this point.
                if (PartExists(validatedUri))
                {
                    ZipPackagePart partToDelete = (ZipPackagePart)GetPart(validatedUri);
 
                    // If the part has one or more piece descriptors, it is interleaved.
                    if (partToDelete.PieceDescriptors.Count > 0)
                    {
                        DeleteInterleavedPartOrStream(partToDelete.PieceDescriptors);
                    }
                }
            }
 
            // We are not absolutely required to clean up all the items in the ignoredItems list,
            // but it will help to clean up incomplete and leftover pieces that belonged to the same
            // part.
            _ignoredItemHelper.Delete(validatedUri);
 
            // Delete the content type for this part if it was specified as an override
            _contentTypeHelper.DeleteContentType(validatedUri);
        }
 
        /// <summary>
        /// This method is for custom implementation specific to the file format.
        /// This is the method that knows how to get the actual parts from the underlying
        /// zip archive.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Some or all of the parts may be interleaved. The Part object for an interleaved part encapsulates
        /// the Uri of the proper part name and the ZipFileInfo of the initial piece.
        /// This function does not go through the extra work of checking piece naming validity
        /// throughout the package.
        /// </para>
        /// <para>
        /// This means that interleaved parts without an initial piece will be silently ignored.
        /// Other naming anomalies get caught at the Stream level when an I/O operation involves
        /// an anomalous or missing piece.
        /// </para>
        /// <para>
        /// This function reads directly from the underlying IO layer and is supposed to be called
        /// just once in the lifetime of a package (at init time).
        /// </para>
        /// </remarks>
        /// <returns>An array of ZipPackagePart.</returns>
        protected override PackagePart[] GetPartsCore()
        {
            List<PackagePart> parts = new List<PackagePart>(InitialPartListSize);
            SortedSet<ZipPackagePartPiece> pieceSet = new SortedSet<ZipPackagePartPiece>();
 
            // The list of files has to be searched linearly (1) to identify the content type
            // stream, and (2) to identify parts.
            System.Collections.ObjectModel.ReadOnlyCollection<ZipArchiveEntry> zipArchiveEntries = _zipArchive.Entries;
 
            // We have already identified the [ContentTypes].xml pieces if any are present during
            // the initialization of ZipPackage object
 
            // Record parts and ignored items.
            foreach (ZipArchiveEntry zipArchiveEntry in zipArchiveEntries)
            {
                //Returns false if -
                // a. its a content type item
                // b. items that have either a leading or trailing slash.
                if (IsZipItemValidOpcPartOrPiece(zipArchiveEntry.FullName))
                {
                    // In the case of a piece name, postpone processing until all piece
                    // candidates have been collected.
                    if (ZipPackagePartPiece.TryParse(zipArchiveEntry, out ZipPackagePartPiece? partPiece))
                    {
                        if (pieceSet.Contains(partPiece))
                        {
                            throw new FormatException(SR.DuplicatePiecesFound);
                        }
 
                        if (partPiece.PartUri != null)
                        {
                            // If a part does not have a valid URI, then we should just ignore it.
                            // It is not meaningful to even add it to the ignored items list as we will
                            // never generate a name that corresponds to this ZIP item and as such will
                            // never have to delete it.
                            pieceSet.Add(partPiece);
                        }
                        continue;
                    }
 
                    Uri partUri = new Uri(GetOpcNameFromZipItemName(zipArchiveEntry.FullName), UriKind.Relative);
                    if (PackUriHelper.TryValidatePartUri(partUri, out PackUriHelper.ValidatedPartUri? validatedPartUri))
                    {
                        ContentType? contentType = _contentTypeHelper.GetContentType(validatedPartUri);
                        if (contentType != null)
                        {
                            // In case there was some redundancy between pieces and/or the atomic
                            // part, it will be detected at this point because the part's Uri (which
                            // is independent of interleaving) will already be in the dictionary.
                            parts.Add(new ZipPackagePart(this, zipArchiveEntry.Archive, zipArchiveEntry,
                                _zipStreamManager, validatedPartUri, contentType.ToString(), GetCompressionOptionFromZipFileInfo()));
                        }
                        else
                        {
                            // Since this part does not have a valid content type we add it to the ignored list,
                            // as later if another part with a similar extension gets added, this part might become
                            // valid the next time we open the package.
                            _ignoredItemHelper.AddItemForAtomicPart(validatedPartUri, zipArchiveEntry.FullName);
                        }
                    }
                    //If not valid part uri we can completely ignore this zip file item. Even if later someone adds
                    //a new part, the corresponding zip item can never map to one of these items
                }
                // If IsZipItemValidOpcPartOrPiece returns false, it implies that either the zip file Item
                // starts or ends with a "/" and as such we can completely ignore this zip file item. Even if later
                // a new part gets added, its corresponding zip item cannot map to one of these items.
            }
 
            // TODO: The original would add ZipItemNames to the ignored item helper if they had folder or volume entries
            // in the ZIP file and were a valid name for a part. We don't do that. I don't think this is a problem.
 
            // Well-formed piece sequences get recorded in parts.
            // Debris from invalid sequences get swept into _ignoredItems.
            ProcessPieces(pieceSet, parts);
 
            return parts.ToArray();
        }
 
        #endregion PackagePart Methods
 
        #region Other Methods
 
        /// <summary>
        /// This method is for custom implementation corresponding to the underlying zip file format.
        /// </summary>
        protected override void FlushCore()
        {
            //Save the content type file to the archive.
            _contentTypeHelper.SaveToFile();
        }
 
        /// <summary>
        /// Closes the underlying ZipArchive object for this container
        /// </summary>
        /// <param name="disposing">True if called during Dispose, false if called during Finalize</param>
        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    _contentTypeHelper?.SaveToFile();
                    _zipArchive?.Dispose();
 
                    // _containerStream may be opened given a file name, in which case it should be closed here.
                    // _containerStream may be passed into the constructor, in which case, it should not be closed here.
                    if (_shouldCloseContainerStream)
                    {
                        _containerStream.Dispose();
                    }
                    _containerStream = null!;
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }
 
        #endregion Other Methods
 
        #endregion Public Methods
 
        #region Internal Constructors
 
        /// <summary>
        /// Internal constructor that is called by the OpenOnFile static method.
        /// </summary>
        /// <param name="path">File path to the container.</param>
        /// <param name="packageFileMode">Container is opened in the specified mode if possible</param>
        /// <param name="packageFileAccess">Container is opened with the specified access if possible</param>
        /// <param name="share">Container is opened with the specified share if possible</param>
 
        internal ZipPackage(string path, FileMode packageFileMode, FileAccess packageFileAccess, FileShare share)
            : base(packageFileAccess)
        {
            ZipArchive? zipArchive = null;
            IgnoredItemHelper? ignoredItemHelper;
            ContentTypeHelper? contentTypeHelper;
            _packageFileMode = packageFileMode;
            _packageFileAccess = packageFileAccess;
 
            try
            {
                _containerStream = new FileStream(path, _packageFileMode, _packageFileAccess, share);
                _shouldCloseContainerStream = true;
                ZipArchiveMode zipArchiveMode = ZipArchiveMode.Update;
                if (packageFileAccess == FileAccess.Read)
                    zipArchiveMode = ZipArchiveMode.Read;
                else if (packageFileAccess == FileAccess.Write)
                    zipArchiveMode = ZipArchiveMode.Create;
                else if (packageFileAccess == FileAccess.ReadWrite)
                    zipArchiveMode = ZipArchiveMode.Update;
 
                zipArchive = new ZipArchive(_containerStream, zipArchiveMode, true);
                _zipStreamManager = new ZipStreamManager(zipArchive, _packageFileMode, _packageFileAccess);
                ignoredItemHelper = new IgnoredItemHelper(zipArchive);
                contentTypeHelper = new ContentTypeHelper(zipArchive, _packageFileMode, _packageFileAccess, _zipStreamManager, ignoredItemHelper);
            }
            catch
            {
                zipArchive?.Dispose();
                _containerStream?.Dispose();
 
                throw;
            }
 
            _zipArchive = zipArchive;
            _ignoredItemHelper = ignoredItemHelper;
            _contentTypeHelper = contentTypeHelper;
        }
 
        /// <summary>
        /// Internal constructor that is called by the Open(Stream) static methods.
        /// </summary>
        /// <param name="s"></param>
        /// <param name="packageFileMode"></param>
        /// <param name="packageFileAccess"></param>
        internal ZipPackage(Stream s, FileMode packageFileMode, FileAccess packageFileAccess)
            : base(packageFileAccess)
        {
            ZipArchive? zipArchive = null;
            IgnoredItemHelper? ignoredItemHelper;
            ContentTypeHelper? contentTypeHelper;
            _packageFileMode = packageFileMode;
            _packageFileAccess = packageFileAccess;
 
            try
            {
                if (s.CanSeek)
                {
                    switch (packageFileMode)
                    {
                        case FileMode.Open:
                            if (s.Length == 0)
                            {
                                throw new FileFormatException(SR.ZipZeroSizeFileIsNotValidArchive);
                            }
                            break;
 
                        case FileMode.CreateNew:
                            if (s.Length != 0)
                            {
                                throw new IOException(SR.CreateNewOnNonEmptyStream);
                            }
                            break;
 
                        case FileMode.Create:
                            if (s.Length != 0)
                            {
                                s.SetLength(0); // Discard existing data
                            }
                            break;
                    }
                }
 
                ZipArchiveMode zipArchiveMode = ZipArchiveMode.Update;
                if (packageFileAccess == FileAccess.Read)
                    zipArchiveMode = ZipArchiveMode.Read;
                else if (packageFileAccess == FileAccess.Write)
                    zipArchiveMode = ZipArchiveMode.Create;
                else if (packageFileAccess == FileAccess.ReadWrite)
                    zipArchiveMode = ZipArchiveMode.Update;
 
                zipArchive = new ZipArchive(s, zipArchiveMode, true);
 
                _zipStreamManager = new ZipStreamManager(zipArchive, packageFileMode, packageFileAccess);
                ignoredItemHelper = new IgnoredItemHelper(zipArchive);
                contentTypeHelper = new ContentTypeHelper(zipArchive, packageFileMode, packageFileAccess, _zipStreamManager, ignoredItemHelper);
            }
            catch (InvalidDataException)
            {
                throw new FileFormatException(SR.FileContainsCorruptedData);
            }
            catch
            {
                zipArchive?.Dispose();
 
                throw;
            }
 
            _containerStream = s;
            _shouldCloseContainerStream = false;
            _zipArchive = zipArchive;
            _ignoredItemHelper = ignoredItemHelper;
            _contentTypeHelper = contentTypeHelper;
        }
 
        #endregion Internal Constructors
 
        #region Internal Methods
 
        // More generic function than GetZipItemNameFromPartName. In particular, it will handle piece names.
        internal static string GetZipItemNameFromOpcName(string opcName)
        {
            Debug.Assert(opcName != null && opcName.Length > 0);
            return opcName.Substring(1);
        }
 
        // More generic function than GetPartNameFromZipItemName. In particular, it will handle piece names.
        internal static string GetOpcNameFromZipItemName(string zipItemName)
        {
            return string.Concat(ForwardSlashString, zipItemName);
        }
 
        // Convert from XPS CompressionOption to ZipFileInfo compression properties.
        internal static void GetZipCompressionMethodFromOpcCompressionOption(
            CompressionOption compressionOption,
            out CompressionLevel compressionLevel)
        {
            switch (compressionOption)
            {
                case CompressionOption.NotCompressed:
                    {
                        compressionLevel = CompressionLevel.NoCompression;
                    }
                    break;
                case CompressionOption.Normal:
                    {
                        compressionLevel = CompressionLevel.Optimal;
                    }
                    break;
                case CompressionOption.Maximum:
                    {
#if NET
                        compressionLevel = CompressionLevel.SmallestSize;
#else
                        compressionLevel = CompressionLevel.Optimal;
#endif
                    }
                    break;
                case CompressionOption.Fast:
                    {
                        compressionLevel = CompressionLevel.Fastest;
                    }
                    break;
                case CompressionOption.SuperFast:
                    {
                        compressionLevel = CompressionLevel.Fastest;
                    }
                    break;
 
                // fall-through is not allowed
                default:
                    {
                        Debug.Fail("Encountered an invalid CompressionOption enum value");
                        goto case CompressionOption.NotCompressed;
                    }
            }
        }
 
        #endregion Internal Methods
 
        internal FileMode PackageFileMode
        {
            get
            {
                return _packageFileMode;
            }
        }
 
        #region Private Methods
 
        //returns a boolean indicating if the underlying zip item is a valid metro part or piece
        // This mainly excludes the content type item, as well as entries with leading or trailing
        // slashes.
        private static bool IsZipItemValidOpcPartOrPiece(string zipItemName)
        {
            Debug.Assert(zipItemName != null, "The parameter zipItemName should not be null");
 
            //check if the zip item is the Content type item -case sensitive comparison
            // The following test will filter out an atomic content type file, with name
            // "[Content_Types].xml", as well as an interleaved one, with piece names such as
            // "[Content_Types].xml/[0].piece" or "[Content_Types].xml/[5].last.piece".
            if (zipItemName.StartsWith(ContentTypeHelper.ContentTypeFileName, StringComparison.OrdinalIgnoreCase))
                return false;
            else
            {
                //Could be an empty zip folder
                //We decided to ignore zip items that contain a "/" as this could be a folder in a zip archive
                //Some of the tools support this and some don't. There is no way ensure that the zip item never have
                //a leading "/", although this is a requirement we impose on items created through our API
                //Therefore we ignore them at the packaging api level.
                if (zipItemName.StartsWith(ForwardSlashString, StringComparison.Ordinal))
                    return false;
                //This will ignore the folder entries found in the zip package created by some zip tool
                //PartNames ending with a "/" slash is also invalid so we are skipping these entries,
                //this will also prevent the PackUriHelper.CreatePartUri from throwing when it encounters a
                // partname ending with a "/"
                if (zipItemName.EndsWith(ForwardSlashString, StringComparison.Ordinal))
                    return false;
                else
                    return true;
            }
        }
 
        // convert from Zip CompressionMethodEnum and DeflateOptionEnum to XPS CompressionOption
        private static CompressionOption GetCompressionOptionFromZipFileInfo()
        {
            // Note: we can't determine compression method / level from the ZipArchiveEntry.
            CompressionOption result = CompressionOption.Normal;
            return result;
        }
 
        private static void DeleteInterleavedPartOrStream(List<ZipPackagePartPiece> sortedPieceInfoList)
        {
            Debug.Assert(sortedPieceInfoList != null);
            if (sortedPieceInfoList.Count > 0)
            {
                foreach (ZipPackagePartPiece pieceInfo in sortedPieceInfoList)
                {
                    pieceInfo.ZipArchiveEntry.Delete();
                }
            }
            // It's okay for us to not clean up the sortedPieceInfoList datastructure, as the
            // owning part is about to be deleted.
        }
 
        /// <summary>
        /// An auxiliary function of GetPartsCore, this function sorts out the piece name
        /// descriptors accumulated in pieceNumber into valid piece sequences and garbage
        /// (i.e. ignorable Zip items).
        /// </summary>
        /// <remarks>
        /// <para>
        /// The procedure used relies on 'pieces' members to be sorted lexicographically
        /// on &lt;name, number, isLast> triples, with name comparisons being case insensitive.
        /// This is enforced by PieceInfo's IComparable implementation.
        /// </para>
        /// </remarks>
        private void ProcessPieces(SortedSet<ZipPackagePartPiece> pieceSet, List<PackagePart> parts)
        {
            // The zip items related to the ContentTypes.xml should have been already processed.
            // Only those zip items that follow the valid piece naming syntax and have a valid
            // part name should show up in this list.
            // piece.PartUri should be non-null
 
            // Exit if nothing to do.
            if (pieceSet.Count == 0)
            {
                return;
            }
 
            string? normalizedPrefixNameForCurrentSequence = null;
            // Value is ignored as long as prefixNameForCurrentSequence is null.
            int startIndexOfCurrentSequence = 0;
            List<ZipPackagePartPiece> pieces = new List<ZipPackagePartPiece>(pieceSet);
 
            for (int i = 0; i < pieces.Count; ++i)
            {
                // Looking for the start of a sequence.
                if (normalizedPrefixNameForCurrentSequence == null)
                {
                    if (pieces[i].PieceNumber != 0)
                    {
                        // Whether or not this piece bears the same unsuffixed name as a complete
                        // sequence just processed, it has to be ignored without reporting an error.
                        _ignoredItemHelper.AddItemForStrayPiece(pieces[i]);
                        continue;
                    }
                    else
                    {
                        // Found the start of a sequence.
                        startIndexOfCurrentSequence = i;
                        normalizedPrefixNameForCurrentSequence = pieces[i].NormalizedPrefixName;
                    }
                }
                // Not a start piece. Carry out validity checks.
                else
                {
                    //Check for incomplete sequence.
                    if (pieces[i].NormalizedPrefixName != normalizedPrefixNameForCurrentSequence)
                    {
                        // Check if the piece we have found is another first piece.
                        if (pieces[i].PieceNumber == 0)
                        {
                            //This can happen when we have an incomplete sequence and we encounter the first piece of the
                            //next sequence
                            _ignoredItemHelper.AddItemsForInvalidSequence(normalizedPrefixNameForCurrentSequence, pieces, startIndexOfCurrentSequence, checked(i - startIndexOfCurrentSequence));
 
                            //Reset these values as we found another first piece
                            startIndexOfCurrentSequence = i;
                            normalizedPrefixNameForCurrentSequence = pieces[i].NormalizedPrefixName;
                        }
                        else
                        {
                            //This can happen when we have an incomplete sequence and the next piece is also
                            //a stray piece. So we can safely ignore all the pieces till this point
                            _ignoredItemHelper.AddItemsForInvalidSequence(normalizedPrefixNameForCurrentSequence, pieces, startIndexOfCurrentSequence, checked(i - startIndexOfCurrentSequence + 1));
                            normalizedPrefixNameForCurrentSequence = null;
                            continue;
                        }
                    }
                    else
                    {
                        //if the names are the same we check if the numbers are increasing
                        if (pieces[i].PieceNumber != i - startIndexOfCurrentSequence)
                        {
                            _ignoredItemHelper.AddItemsForInvalidSequence(normalizedPrefixNameForCurrentSequence, pieces, startIndexOfCurrentSequence, checked(i - startIndexOfCurrentSequence + 1));
                            normalizedPrefixNameForCurrentSequence = null;
                            continue;
                        }
                    }
                }
 
                // Looking for the end of a sequence (i.e. a .last suffix).
                if (pieces[i].IsLastPiece)
                {
                    // Record sequence just seen.
                    RecordValidSequence(
                        normalizedPrefixNameForCurrentSequence,
                        pieces,
                        startIndexOfCurrentSequence,
                        i - startIndexOfCurrentSequence + 1,
                        parts);
 
                    // Resume searching for a new sequence.
                    normalizedPrefixNameForCurrentSequence = null;
                }
            }
 
            // clean up any pieces that might be at the end that do not make a complete sequence
            // This can happen when we find a valid piece zero and/or a few other pieces but not
            // the complete sequence, right at the end of the pieces list and we will finish the
            // for loop
            if (normalizedPrefixNameForCurrentSequence != null)
            {
                _ignoredItemHelper.AddItemsForInvalidSequence(normalizedPrefixNameForCurrentSequence, pieces, startIndexOfCurrentSequence, checked(pieces.Count - startIndexOfCurrentSequence));
            }
        }
 
        /// <summary>
        /// The sequence of numItems starting at startIndex can be assumed valid
        /// from the point of view of piece-naming suffixes.
        /// This method makes sure a valid Uri and content type can be inferred
        /// from the name of the first piece. If so, a ZipPackagePart is created
        /// and added to the list 'parts'. If not, the piece names are recorded
        /// as ignorable items.
        /// </summary>
        /// <remarks>
        /// When the sequence and Uri are valid but there is no content type, the
        /// part name is recorded in a specific list of null-content type parts.
        /// </remarks>
        private void RecordValidSequence(
            string normalizedPrefixNameForCurrentSequence,
            List<ZipPackagePartPiece> pieces,
            int startIndex,
            int numItems,
            List<PackagePart> parts)
        {
            // The Uri and content type are inferred from the unsuffixed name of the
            // first piece.
            PackUriHelper.ValidatedPartUri partUri = pieces[startIndex].PartUri!;
            ContentType? contentType = _contentTypeHelper.GetContentType(partUri);
            if (contentType == null)
            {
                _ignoredItemHelper.AddItemsForInvalidSequence(normalizedPrefixNameForCurrentSequence, pieces, startIndex, numItems);
                return;
            }
 
            // Add a new part, initializing with an array of PieceInfo.
            parts.Add(new ZipPackagePart(this, _zipArchive, _zipStreamManager, pieces.GetRange(startIndex, numItems), partUri, contentType.ToString(),
                GetCompressionOptionFromZipFileInfo()));
        }
 
        #endregion Private Methods
 
        #region Private Members
 
        private const int InitialPartListSize = 50;
 
        private readonly ZipArchive _zipArchive;
        private Stream _containerStream;      // stream we are opened in if Open(Stream) was called
        private readonly bool _shouldCloseContainerStream;
        private readonly ContentTypeHelper _contentTypeHelper;    // manages the content types for all the parts in the container
        private readonly IgnoredItemHelper _ignoredItemHelper;    // manages the ignored items in a ZIP package
        private readonly ZipStreamManager _zipStreamManager;      // manages streams for all parts, avoiding opening streams multiple times
        private readonly FileAccess _packageFileAccess;
        private readonly FileMode _packageFileMode;
 
        private const string ForwardSlashString = "/"; //Required for creating a part name from a zip item name
 
        //IEqualityComparer for extensions
        private static readonly ExtensionEqualityComparer s_extensionEqualityComparer = new ExtensionEqualityComparer();
 
        #endregion Private Members
 
        /// <summary>
        /// ExtensionComparer
        /// The Extensions are stored in the Default Dictionary in their original form,
        /// however they are compared in a normalized manner.
        /// Equivalence for extensions in the content type stream, should follow
        /// the same rules as extensions of partnames. Also, by the time this code is invoked,
        /// we have already validated, that the extension is in the correct format as per the
        /// part name rules.So we are simplifying the logic here to just convert the extensions
        /// to Upper invariant form and then compare them.
        /// </summary>
        private sealed class ExtensionEqualityComparer : IEqualityComparer<string>
        {
            bool IEqualityComparer<string>.Equals(string? extensionA, string? extensionB)
            {
                Debug.Assert(extensionA != null, "extension should not be null");
                Debug.Assert(extensionB != null, "extension should not be null");
 
                //Important Note: any change to this should be made in accordance
                //with the rules for comparing/normalizing partnames.
                //Refer to PackUriHelper.ValidatedPartUri.GetNormalizedPartUri method.
                //Currently normalization just involves upper-casing ASCII and hence the simplification.
                return extensionA.Equals(extensionB, StringComparison.OrdinalIgnoreCase);
            }
 
            int IEqualityComparer<string>.GetHashCode(string extension)
            {
                Debug.Assert(extension != null, "extension should not be null");
 
                //Important Note: any change to this should be made in accordance
                //with the rules for comparing/normalizing partnames.
                //Refer to PackUriHelper.ValidatedPartUri.GetNormalizedPartUri method.
                //Currently normalization just involves upper-casing ASCII and hence the simplification.
                return extension.ToUpperInvariant().GetHashCode();
            }
        }
 
        /// <summary>
        /// This is a helper class that maintains the Content Types File related to
        /// this ZipPackage.
        /// </summary>
        private sealed class ContentTypeHelper
        {
            /// <summary>
            /// Initialize the object without uploading any information from the package.
            /// Complete initialization in read mode also involves calling ParseContentTypesFile
            /// to deserialize content type information.
            /// </summary>
            internal ContentTypeHelper(ZipArchive zipArchive, FileMode packageFileMode, FileAccess packageFileAccess, ZipStreamManager zipStreamManager, IgnoredItemHelper ignoredItemHelper)
            {
                _zipArchive = zipArchive;               //initialized in the ZipPackage constructor
                _packageFileMode = packageFileMode;
                _packageFileAccess = packageFileAccess;
                _zipStreamManager = zipStreamManager;   //initialized in the ZipPackage constructor
                // The extensions are stored in the default Dictionary in their original form , but they are compared
                // in a normalized manner using the ExtensionComparer.
                _defaultDictionary = new Dictionary<string, ContentType>(DefaultDictionaryInitialSize, s_extensionEqualityComparer);
 
                _ignoredItemHelper = ignoredItemHelper;
 
                // Identify the content type file or files before identifying parts and piece sequences.
                // This is necessary because the name of the content type stream is not a part name and
                // the information it contains is needed to recognize valid parts.
                if (_zipArchive.Mode == ZipArchiveMode.Read || _zipArchive.Mode == ZipArchiveMode.Update)
                    ParseContentTypesFile(_zipArchive.Entries);
 
                //No contents to persist to the disk -
                _dirty = false; //by default
 
                //Lazy initialize these members as required
                //_overrideDictionary      - Overrides should be rare
                //_contentTypeFileInfo     - We will either find an atomin part, or
                //_contentTypeStreamPieces - an interleaved part
                //_contentTypeStreamExists - defaults to false - not yet found
            }
 
            internal static string ContentTypeFileName
            {
                get
                {
                    return ContentTypesFile;
                }
            }
 
            //Adds the Default entry if it is the first time we come across
            //the extension for the partUri, does nothing if the content type
            //corresponding to the default entry for the extension matches or
            //adds a override corresponding to this part and content type.
            //This call is made when a new part is being added to the package.
 
            // This method assumes the partUri is valid.
            internal void AddContentType(PackUriHelper.ValidatedPartUri partUri, ContentType contentType,
                CompressionLevel compressionLevel)
            {
                //save the compressionOption and deflateOption that should be used
                //to create the content type item later
                if (!_contentTypeStreamExists)
                {
                    _cachedCompressionLevel = compressionLevel;
                }
 
                // Figure out whether the mapping matches a default entry, can be made into a new
                // default entry, or has to be entered as an override entry.
                bool foundMatchingDefault = false;
                string extension = partUri.PartUriExtension;
 
                // Need to create an override entry?
                if (extension.Length == 0
                    || (_defaultDictionary.TryGetValue(extension, out ContentType? value)
                        && !(foundMatchingDefault = value.AreTypeAndSubTypeEqual(contentType))))
                {
                    AddOverrideElement(partUri, contentType);
                }
 
                // Else, either there is already a mapping from extension to contentType,
                // or one needs to be created.
                else if (!foundMatchingDefault)
                {
                    AddDefaultElement(extension, contentType);
 
                    // Delete all items that might map to the same extension, as these currently
                    // ignored items might show up as valid parts later.
                    _ignoredItemHelper.DeleteItemsWithSimilarExtension(extension);
                }
            }
 
 
            //Returns the content type for the part, if present, else returns null.
            internal ContentType? GetContentType(PackUriHelper.ValidatedPartUri partUri)
            {
                //Step 1: Check if there is an override entry present corresponding to the
                //partUri provided. Override takes precedence over the default entries
                if (_overrideDictionary != null)
                {
                    if (_overrideDictionary.TryGetValue(partUri, out ContentType? val))
                        return val;
                }
 
                //Step 2: Check if there is a default entry corresponding to the
                //extension of the partUri provided.
                string extension = partUri.PartUriExtension;
 
                if (_defaultDictionary.TryGetValue(extension, out ContentType? value))
                    return value;
 
                //Step 3: If we did not find an entry in the override and the default
                //dictionaries, this is an error condition
                return null;
            }
 
            //Deletes the override entry corresponding to the partUri, if it exists
            internal void DeleteContentType(PackUriHelper.ValidatedPartUri partUri)
            {
                if (_overrideDictionary != null)
                {
                    if (_overrideDictionary.Remove(partUri))
                        _dirty = true;
                }
            }
 
            internal void SaveToFile()
            {
                if (_dirty)
                {
                    //Lazy init: Initialize when the first part is added.
                    if (!_contentTypeStreamExists)
                    {
                        _contentTypeZipArchiveEntry = _zipArchive.CreateEntry(ContentTypesFile, _cachedCompressionLevel);
                        _contentTypeStreamExists = true;
                    }
                    else
                    {
                        // delete and re-create entry for content part.  When writing this, the stream will not truncate the content
                        // if the XML is shorter than the existing content part.
 
                        if (_contentTypeStreamPieces != null)
                        {
                            // Delete all part pieces for content part.
                            DeleteInterleavedPartOrStream(_contentTypeStreamPieces);
                        }
                        else
                        {
                            // Atomic name
                            _contentTypeZipArchiveEntry!.Delete();
                        }
                        _contentTypeZipArchiveEntry = _zipArchive.CreateEntry(ContentTypesFile);
                    }
 
                    using (Stream s = _zipStreamManager.Open(_contentTypeZipArchiveEntry, FileAccess.ReadWrite))
                    {
                        // use UTF-8 encoding by default
                        using (XmlWriter writer = XmlWriter.Create(s, new XmlWriterSettings { Encoding = System.Text.Encoding.UTF8 }))
                        {
                            writer.WriteStartDocument();
 
                            // write root element tag - Types
                            writer.WriteStartElement(TypesTagName, TypesNamespaceUri);
 
                            // for each default entry
                            foreach (string key in _defaultDictionary.Keys)
                            {
                                WriteDefaultElement(writer, key, _defaultDictionary[key]);
                            }
 
                            if (_overrideDictionary != null)
                            {
                                // for each override entry
                                foreach (PackUriHelper.ValidatedPartUri key in _overrideDictionary.Keys)
                                {
                                    WriteOverrideElement(writer, key, _overrideDictionary[key]);
                                }
                            }
 
                            // end of Types tag
                            writer.WriteEndElement();
 
                            // close the document
                            writer.WriteEndDocument();
 
                            _dirty = false;
                        }
                    }
                }
            }
 
            [MemberNotNull(nameof(_overrideDictionary))]
            private void EnsureOverrideDictionary()
            {
                // The part Uris are stored in the Override Dictionary in their original form , but they are compared
                // in a normalized manner using the PartUriComparer
                _overrideDictionary ??= new Dictionary<PackUriHelper.ValidatedPartUri, ContentType>(OverrideDictionaryInitialSize);
            }
 
            private void ParseContentTypesFile(System.Collections.ObjectModel.ReadOnlyCollection<ZipArchiveEntry> zipFiles)
            {
                // Find the content type stream, allowing for interleaving. Naming collisions
                // (as between an atomic and an interleaved part) will result in an exception being thrown.
                Stream? s = OpenContentTypeStream(zipFiles);
 
                // Allow non-existent content type stream.
                if (s == null)
                    return;
 
                XmlReaderSettings xrs = new XmlReaderSettings();
                xrs.IgnoreWhitespace = true;
 
                using (s)
                using (XmlReader reader = XmlReader.Create(s, xrs))
                {
                    //This method expects the reader to be in ReadState.Initial.
                    //It will make the first read call.
                    PackagingUtilities.PerformInitialReadAndVerifyEncoding(reader);
 
                    //Note: After the previous method call the reader should be at the first tag in the markup.
                    //MoveToContent - Skips over the following - ProcessingInstruction, DocumentType, Comment, Whitespace, or SignificantWhitespace
                    //If the reader is currently at a content node then this function call is a no-op
                    reader.MoveToContent();
 
                    // look for our root tag and namespace pair - ignore others in case of version changes
                    // Make sure that the current node read is an Element
                    if ((reader.NodeType == XmlNodeType.Element)
                        && (reader.Depth == 0)
                        && (reader.NamespaceURI == TypesNamespaceUri)
                        && (reader.Name == TypesTagName))
                    {
                        //There should be a namespace Attribute present at this level.
                        //Also any other attribute on the <Types> tag is an error including xml: and xsi: attributes
                        if (PackagingUtilities.GetNonXmlnsAttributeCount(reader) > 0)
                        {
                            throw new XmlException(SR.TypesTagHasExtraAttributes, null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
                        }
 
                        // start tag encountered
                        // now parse individual Default and Override tags
                        while (reader.Read())
                        {
                            //Skips over the following - ProcessingInstruction, DocumentType, Comment, Whitespace, or SignificantWhitespace
                            //If the reader is currently at a content node then this function call is a no-op
                            reader.MoveToContent();
 
                            //If MoveToContent() takes us to the end of the content
                            if (reader.NodeType == XmlNodeType.None)
                                continue;
 
                            // Make sure that the current node read is an element
                            // Currently we expect the Default and Override Tag at Depth 1
                            if (reader.NodeType == XmlNodeType.Element
                                && reader.Depth == 1
                                && (reader.NamespaceURI == TypesNamespaceUri)
                                && (reader.Name == DefaultTagName))
                            {
                                ProcessDefaultTagAttributes(reader);
                            }
                            else if (reader.NodeType == XmlNodeType.Element
                                     && reader.Depth == 1
                                     && (reader.NamespaceURI == TypesNamespaceUri)
                                     && (reader.Name == OverrideTagName))
                            {
                                ProcessOverrideTagAttributes(reader);
                            }
                            else if (reader.NodeType == XmlNodeType.EndElement && reader.Depth == 0 && reader.Name == TypesTagName)
                            {
                                continue;
                            }
                            else
                            {
                                throw new XmlException(SR.TypesXmlDoesNotMatchSchema, null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
                            }
                        }
                    }
                    else
                    {
                        throw new XmlException(SR.TypesElementExpected, null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
                    }
                }
            }
 
            /// <summary>
            /// Find the content type stream, allowing for interleaving. Naming collisions
            /// (as between an atomic and an interleaved part) will result in an exception being thrown.
            /// Return null if no content type stream has been found.
            /// </summary>
            /// <remarks>
            /// The input array is lexicographically sorted
            /// </remarks>
            private Stream? OpenContentTypeStream(System.Collections.ObjectModel.ReadOnlyCollection<ZipArchiveEntry> zipFiles)
            {
                // Collect all pieces found prior to sorting and validating the sequence.
                SortedDictionary<ZipPackagePartPiece, ZipArchiveEntry>? contentTypeStreamPieces = null;
 
                foreach (ZipArchiveEntry zipFileInfo in zipFiles)
                {
                    if (zipFileInfo.FullName.StartsWith(ContentTypesFileUpperInvariant, StringComparison.OrdinalIgnoreCase))
                    {
                        // Atomic name.
                        if (zipFileInfo.FullName.Length == ContentTypeFileName.Length)
                        {
                            // Record the file info.
                            _contentTypeZipArchiveEntry = zipFileInfo;
                        }
                        else if (ZipPackagePartPiece.TryParse(zipFileInfo, out ZipPackagePartPiece? pieceInfo))
                        {
                            // Lazy init.
                            contentTypeStreamPieces ??= new SortedDictionary<ZipPackagePartPiece, ZipArchiveEntry>();
 
                            // Record the piece info.
                            contentTypeStreamPieces.Add(pieceInfo, zipFileInfo);
                        }
                    }
                }
 
                List<ZipPackagePartPiece>? partPieces = null;
 
                // If pieces were found, find out if there is a piece 0, in which case
                // sequence validity will be required.
                // Since the general case requires a sorted array, use a sorted array for this.
                if (contentTypeStreamPieces != null)
                {
                    partPieces = new List<ZipPackagePartPiece>(contentTypeStreamPieces.Keys);
 
                    // Negative piece numbers are invalid, so if piece 0 occurs at all, it occurs first.
                    if (partPieces[0].PieceNumber != 0)
                    {
                        // The pieces we have found form an incomplete sequence as the first
                        // piece is missing. So owe add these to the list of ignored items.
                        _ignoredItemHelper.AddItemsForInvalidSequence(ContentTypesFileUpperInvariant, partPieces, 0, partPieces.Count);
 
                        partPieces = null;
                    }
                    else
                    {
                        // Check piece numbers and end indicator.
                        int lastPieceNumber = -1;
 
                        for (int pieceNumber = 0; pieceNumber < partPieces.Count; ++pieceNumber)
                        {
                            if (partPieces[pieceNumber].PieceNumber != pieceNumber)
                            {
                                _ignoredItemHelper.AddItemsForInvalidSequence(ContentTypesFileUpperInvariant, partPieces, 0, partPieces.Count);
 
                                partPieces = null;
                                break;
                            }
 
                            if (partPieces[pieceNumber].IsLastPiece)
                            {
                                lastPieceNumber = pieceNumber;
                                break;
                            }
                        }
 
                        if (partPieces != null)
                        {
                            // Last piece not found
                            if (lastPieceNumber == -1)
                            {
                                _ignoredItemHelper.AddItemsForInvalidSequence(ContentTypesFileUpperInvariant, partPieces, 0, partPieces.Count);
 
                                partPieces = null;
                            }
                            else
                            {
                                // Add any extra items after the last piece to the ignored items list.
                                if (lastPieceNumber < partPieces.Count - 1)
                                {
                                    // The pieces we have found are extra pieces after a last piece has been found.
                                    // So we add all the extra pieces to the ignored item list.
                                    _ignoredItemHelper.AddItemsForInvalidSequence(ContentTypesFileUpperInvariant, partPieces, lastPieceNumber + 1, partPieces.Count - lastPieceNumber - 1);
                                    partPieces.RemoveRange(lastPieceNumber + 1, partPieces.Count - lastPieceNumber - 1);
                                }
                            }
                        }
                    }
                }
 
                // If an atomic file was found, open a stream on it.
                if (_contentTypeZipArchiveEntry != null)
                {
                    // Detect conflict with piece name(s).
                    if (contentTypeStreamPieces != null)
                    {
                        throw new FormatException(SR.BadPackageFormat);
                    }
 
                    _contentTypeStreamExists = true;
                    return _zipStreamManager.Open(_contentTypeZipArchiveEntry, FileAccess.ReadWrite);
                }
                // If the content type stream is interleaved, validate the piece numbering.
                else if (partPieces != null)
                {
                    _contentTypeStreamExists = true;
                    _contentTypeStreamPieces = partPieces;
 
                    return new InterleavedZipPackagePartStream(_zipStreamManager, _contentTypeStreamPieces, FileAccess.Read);
                }
 
                // No content type stream was found.
                return null;
            }
 
            // Process the attributes for the Default tag
            private void ProcessDefaultTagAttributes(XmlReader reader)
            {
                //There could be a namespace Attribute present at this level.
                //Also any other attribute on the <Default> tag is an error including xml: and xsi: attributes
                if (PackagingUtilities.GetNonXmlnsAttributeCount(reader) != 2)
                    throw new XmlException(SR.DefaultTagDoesNotMatchSchema, null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
 
                // get the required Extension and ContentType attributes
 
                string? extensionAttributeValue = reader.GetAttribute(ExtensionAttributeName);
                ValidateXmlAttribute(ExtensionAttributeName, extensionAttributeValue, DefaultTagName, reader);
 
                string? contentTypeAttributeValue = reader.GetAttribute(ContentTypeAttributeName);
                ThrowIfXmlAttributeMissing(ContentTypeAttributeName, contentTypeAttributeValue, DefaultTagName, reader);
 
                // The extensions are stored in the Default Dictionary in their original form , but they are compared
                // in a normalized manner using the ExtensionComparer.
                PackUriHelper.ValidatedPartUri temporaryUri = PackUriHelper.ValidatePartUri(
                    new Uri(TemporaryPartNameWithoutExtension + extensionAttributeValue, UriKind.Relative));
                _defaultDictionary.Add(temporaryUri.PartUriExtension, new ContentType(contentTypeAttributeValue!));
 
                //Skip the EndElement for Default Tag
                if (!reader.IsEmptyElement)
                    ProcessEndElement(reader, DefaultTagName);
            }
 
            // Process the attributes for the Default tag
            private void ProcessOverrideTagAttributes(XmlReader reader)
            {
                //There could be a namespace Attribute present at this level.
                //Also any other attribute on the <Override> tag is an error including xml: and xsi: attributes
                if (PackagingUtilities.GetNonXmlnsAttributeCount(reader) != 2)
                    throw new XmlException(SR.OverrideTagDoesNotMatchSchema, null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
 
                // get the required Extension and ContentType attributes
                string? partNameAttributeValue = reader.GetAttribute(PartNameAttributeName);
                ValidateXmlAttribute(PartNameAttributeName, partNameAttributeValue, OverrideTagName, reader);
 
                string? contentTypeAttributeValue = reader.GetAttribute(ContentTypeAttributeName);
                ThrowIfXmlAttributeMissing(ContentTypeAttributeName, contentTypeAttributeValue, OverrideTagName, reader);
 
                PackUriHelper.ValidatedPartUri partUri = PackUriHelper.ValidatePartUri(new Uri(partNameAttributeValue!, UriKind.Relative));
 
                //Lazy initializing - ensure that the override dictionary has been initialized
                EnsureOverrideDictionary();
 
                // The part Uris are stored in the Override Dictionary in their original form , but they are compared
                // in a normalized manner using PartUriComparer.
                _overrideDictionary.Add(partUri, new ContentType(contentTypeAttributeValue!));
 
                //Skip the EndElement for Override Tag
                if (!reader.IsEmptyElement)
                    ProcessEndElement(reader, OverrideTagName);
            }
 
            //If End element is present for Relationship then we process it
            private static void ProcessEndElement(XmlReader reader, string elementName)
            {
                Debug.Assert(!reader.IsEmptyElement, "This method should only be called it the Relationship Element is not empty");
 
                reader.Read();
 
                //Skips over the following - ProcessingInstruction, DocumentType, Comment, Whitespace, or SignificantWhitespace
                reader.MoveToContent();
 
                if (reader.NodeType == XmlNodeType.EndElement && elementName == reader.LocalName)
                    return;
                else
                    throw new XmlException(SR.Format(SR.ElementIsNotEmptyElement, elementName), null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
            }
 
            private void AddOverrideElement(PackUriHelper.ValidatedPartUri partUri, ContentType contentType)
            {
                //Delete any entry corresponding in the Override dictionary
                //corresponding to the PartUri for which the contentType is being added.
                //This is to compensate for dead override entries in the content types file.
                DeleteContentType(partUri);
 
                //Lazy initializing - ensure that the override dictionary has been initialized
                EnsureOverrideDictionary();
 
                // The part Uris are stored in the Override Dictionary in their original form , but they are compared
                // in a normalized manner using PartUriComparer.
                _overrideDictionary.Add(partUri, contentType);
                _dirty = true;
            }
 
            private void AddDefaultElement(string extension, ContentType contentType)
            {
                // The extensions are stored in the Default Dictionary in their original form , but they are compared
                // in a normalized manner using the ExtensionComparer.
                _defaultDictionary.Add(extension, contentType);
 
                _dirty = true;
            }
 
            private static void WriteOverrideElement(XmlWriter xmlWriter, PackUriHelper.ValidatedPartUri partUri, ContentType contentType)
            {
                xmlWriter.WriteStartElement(OverrideTagName);
                xmlWriter.WriteAttributeString(PartNameAttributeName,
                    partUri.PartUriString);
                xmlWriter.WriteAttributeString(ContentTypeAttributeName, contentType.ToString());
                xmlWriter.WriteEndElement();
            }
 
            private static void WriteDefaultElement(XmlWriter xmlWriter, string extension, ContentType contentType)
            {
                xmlWriter.WriteStartElement(DefaultTagName);
                xmlWriter.WriteAttributeString(ExtensionAttributeName, extension);
                xmlWriter.WriteAttributeString(ContentTypeAttributeName, contentType.ToString());
                xmlWriter.WriteEndElement();
            }
 
            //Validate if the required XML attribute is present and not an empty string
            private static void ValidateXmlAttribute(string attributeName, string? attributeValue, string tagName, XmlReader reader)
            {
                ThrowIfXmlAttributeMissing(attributeName, attributeValue, tagName, reader);
 
                //Checking for empty attribute
                if (attributeValue!.Length == 0)
                    throw new XmlException(SR.Format(SR.RequiredAttributeEmpty, tagName, attributeName), null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
            }
 
 
            //Validate if the required Content type XML attribute is present
            //Content type of a part can be empty
            private static void ThrowIfXmlAttributeMissing(string attributeName, string? attributeValue, string tagName, XmlReader reader)
            {
                if (attributeValue == null)
                    throw new XmlException(SR.Format(SR.RequiredAttributeMissing, tagName, attributeName), null, ((IXmlLineInfo)reader).LineNumber, ((IXmlLineInfo)reader).LinePosition);
            }
 
            private Dictionary<PackUriHelper.ValidatedPartUri, ContentType>? _overrideDictionary;
            private readonly Dictionary<string, ContentType> _defaultDictionary;
            private readonly ZipArchive _zipArchive;
            private readonly IgnoredItemHelper _ignoredItemHelper;
            private readonly FileMode _packageFileMode;
            private readonly FileAccess _packageFileAccess;
            private readonly ZipStreamManager _zipStreamManager;
            private ZipArchiveEntry? _contentTypeZipArchiveEntry;
            private List<ZipPackagePartPiece>? _contentTypeStreamPieces;
            private bool _contentTypeStreamExists;
            private bool _dirty;
            private CompressionLevel _cachedCompressionLevel;
            private const string ContentTypesFile = "[Content_Types].xml";
            private const string ContentTypesFileUpperInvariant = "[CONTENT_TYPES].XML";
            private const int DefaultDictionaryInitialSize = 16;
            private const int OverrideDictionaryInitialSize = 8;
 
            //Xml tag specific strings for the Content Type file
            private const string TypesNamespaceUri = "http://schemas.openxmlformats.org/package/2006/content-types";
            private const string TypesTagName = "Types";
            private const string DefaultTagName = "Default";
            private const string ExtensionAttributeName = "Extension";
            private const string ContentTypeAttributeName = "ContentType";
            private const string OverrideTagName = "Override";
            private const string PartNameAttributeName = "PartName";
            private const string TemporaryPartNameWithoutExtension = "/tempfiles/sample.";
        }
 
        /// <summary>
        /// This class is used to maintain a list of the zip items that currently do not
        /// map to a part name or [ContentTypes].xml. These items may get added to the ignored
        /// items list for one of the reasons -
        /// a. If the item encountered is a volume lable or folder in the zip archive and has a
        ///    valid part name
        /// b. If the interleaved sequence encountered is incomplete
        /// c. If the atomic piece or complete interleaved sequence encountered
        ///    does not have a corresponding content type.
        /// d. If the are extra pieces that are found after encountering the last piece for a
        ///    sequence.
        ///
        /// These items are subject to deletion if -
        /// i.   A part with a similar prefix name gets added to the package and as such we
        ///      need to delete the existing items so that there will be no naming conflict and
        ///      we can safely at the new part.
        /// ii.  A part with an extension that matches to some of the items in the ingnored list.
        ///      We need to delete these items so that they do not show up as actual parts next
        ///      time the package is opened.
        /// iii. A part that is getting deleted, we clean up the leftover sequences that might be
        ///      present as well
        ///
        /// The same helper class object is used to maintain the ignored pieces corresponding to
        /// valid part name prefixes and the [ContentTypes].xml prefix
        /// </summary>
        private sealed class IgnoredItemHelper
        {
            private const int _dictionaryInitialSize = 8;
            private const int _listInitialSize = 1;
 
            //dictionary mapping a normalized prefix name to different items
            //with the same prefix name.
            private readonly Dictionary<string, List<string>> _ignoredItemDictionary;
 
            //using an additional extension dictionary to map an extenstion to
            //different prefix names with the same extension, in order to
            //reduce the string parsing
            private readonly Dictionary<string, List<string>> _extensionDictionary;
 
 
            private readonly ZipArchive _zipArchive;
 
            /// <summary>
            /// IgnoredItemHelper - private class to keep track of all the items in the
            /// zipArchive that can be ignored and might need to be deleted later.
            /// </summary>
            /// <param name="zipArchive"></param>
            internal IgnoredItemHelper(ZipArchive zipArchive)
            {
                _extensionDictionary = new Dictionary<string, List<string>>(_dictionaryInitialSize, s_extensionEqualityComparer);
                _ignoredItemDictionary = new Dictionary<string, List<string>>(_dictionaryInitialSize, StringComparer.Ordinal);
                _zipArchive = zipArchive;
            }
 
            /// <summary>
            /// Adds a partUri and zipFilename pair that corresponds to one of the following -
            /// 1. A zipFile item that has a valid part name, but does no have a content type
            /// 2. A zipFile item that may be a volume or a folder entry, that has a valid part name
            /// </summary>
            /// <param name="partUri">partUri of the item</param>
            /// <param name="zipFileName">actual zipFileName</param>
            internal void AddItemForAtomicPart(PackUriHelper.ValidatedPartUri partUri, string zipFileName)
            {
                AddItem(partUri, partUri.NormalizedPartUriString, zipFileName);
            }
 
            /// <summary>
            /// Adds an entry corresponding to the pieceInfo to the ignoredItems list if -
            /// 1. We encounter random piece items that are not a part of a complete sequence
            /// </summary>
            /// <param name="pieceInfo">pieceInfo of the item to be ignored</param>
            internal void AddItemForStrayPiece(ZipPackagePartPiece pieceInfo)
            {
                AddItem(pieceInfo.PartUri, pieceInfo.NormalizedPrefixName, pieceInfo.ZipArchiveEntry.FullName);
            }
 
            /// <summary>
            /// Adds an entry corresponding to the prefix name when -
            /// 1. An invalid sequence is encountered, we record the entire sequence to be ignored.
            /// 2. If there is no content type for a valid sequence
            /// </summary>
            /// <param name="normalizedPrefixNameForThisSequence"></param>
            /// <param name="pieces"></param>
            /// <param name="startIndex"></param>
            /// <param name="count"></param>
            internal void AddItemsForInvalidSequence(string normalizedPrefixNameForThisSequence, List<ZipPackagePartPiece> pieces, int startIndex, int count)
            {
                if (!_ignoredItemDictionary.TryGetValue(normalizedPrefixNameForThisSequence, out List<string>? zipFileInfoNameList))
                {
                    zipFileInfoNameList = new List<string>(count);
                    _ignoredItemDictionary.Add(normalizedPrefixNameForThisSequence, zipFileInfoNameList);
                }
 
                //there is no suitable List<>.AddRange method that we can use, so have to add
                //using a "for" loop
                for (int i = startIndex; i < startIndex + count; ++i)
                {
                    zipFileInfoNameList.Add(pieces[i].ZipArchiveEntry.FullName);
                }
 
                //If we are adding ignored items where the prefix name maps to the valid part name
                //the we update the extension dictionary as well
                if (pieces[startIndex].PartUri != null)
                {
                    UpdateExtensionDictionary(pieces[startIndex].PartUri!, pieces[startIndex].NormalizedPrefixName);
                }
            }
 
            /// <summary>
            /// Delete all the items in the underlying archive that might have the same
            /// normalized name as that of the part being added.
            /// </summary>
            /// <param name="partUri"></param>
            internal void Delete(PackUriHelper.ValidatedPartUri partUri)
                => DeleteCore(partUri.NormalizedPartUriString);
 
            private void DeleteCore(string normalizedPartName)
            {
                if (_ignoredItemDictionary.TryGetValue(normalizedPartName, out List<string>? zipFileInfoNames))
                {
                    foreach (string zipFileInfoName in zipFileInfoNames)
                    {
                        ZipArchiveEntry? entry = _zipArchive.GetEntry(zipFileInfoName);
 
                        entry?.Delete();
                    }
 
                    _ignoredItemDictionary.Remove(normalizedPartName);
                }
            }
 
            /// <summary>
            /// If we are adding a new content type then we should delete all the items
            /// in the ignored items list that might have the similar content
            /// </summary>
            /// <param name="extension"></param>
            internal void DeleteItemsWithSimilarExtension(string extension)
            {
                if (_extensionDictionary.TryGetValue(extension, out List<string>? normalizedPartNames))
                {
                    foreach (string normalizedPartName in normalizedPartNames)
                    {
                        DeleteCore(normalizedPartName);
                    }
                    _extensionDictionary.Remove(extension);
                }
            }
 
            private void AddItem(PackUriHelper.ValidatedPartUri? partUri, string normalizedPrefixName, string zipFileName)
            {
                if (!_ignoredItemDictionary.ContainsKey(normalizedPrefixName))
                    _ignoredItemDictionary.Add(normalizedPrefixName, new List<string>(_listInitialSize));
 
                _ignoredItemDictionary[normalizedPrefixName].Add(zipFileName);
 
                //If we are adding ignored items where the prefix name maps to the valid part name
                //the we update the extension dictionary as well
                if (partUri != null)
                    UpdateExtensionDictionary(partUri, normalizedPrefixName);
            }
 
            private void UpdateExtensionDictionary(PackUriHelper.ValidatedPartUri partUri, string normalizedPrefixName)
            {
                string extension = partUri.PartUriExtension;
 
                if (!_extensionDictionary.ContainsKey(extension))
                    _extensionDictionary.Add(extension, new List<string>(_listInitialSize));
 
                _extensionDictionary[extension].Add(normalizedPrefixName);
            }
        }
    }
}