File: PackageExtractor.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Packaging.Core;
using NuGet.Packaging.Signing;
using NuGet.Shared;

namespace NuGet.Packaging
{
    public static class PackageExtractor
    {
        /// <summary>
        /// Extracts a package, using the packages.config directory layout
        /// </summary>
        /// <remarks>For PackageReference directory layout, use <see cref="PackageExtractor.InstallFromSourceAsync(string, PackageIdentity, Func{Stream, Task}, VersionFolderPathResolver, PackageExtractionContext, CancellationToken, Guid)"/></remarks>
        /// <param name="source">The source from which the package was downloaded</param>
        /// <param name="packageStream">A <see cref="Stream"/> for the nupkg file that is being extracted.</param>
        /// <param name="packagePathResolver">The V2 (packages.config) <see cref="PackagePathResolver"/>.</param>
        /// <param name="packageExtractionContext">The <see cref="PackageExtractionContext"/> with settings for how the extraction should be configured.</param>
        /// <param name="token">A cancellation token.</param>
        /// <param name="parentId">Telemetry parent ID.</param>
        /// <returns>A collection of files that were extracted.</returns>
        /// <exception cref="ArgumentNullException">If packageReader, packagePathResolver, or packageExtractionContext are null.</exception>
        /// <exception cref="ArgumentException">If packageStream is not seekable.</exception>
        /// <exception cref="SignatureException">If the package signature couldn't be validated. See exception message for more details.</exception>
        public static async Task<IEnumerable<string>> ExtractPackageAsync(
            string source,
            Stream packageStream,
            PackagePathResolver packagePathResolver,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token,
            Guid parentId = default(Guid))
        {
            if (packageStream == null)
            {
                throw new ArgumentNullException(nameof(packageStream));
            }

            if (!packageStream.CanSeek)
            {
                throw new ArgumentException(Strings.PackageStreamShouldBeSeekable);
            }

            if (packagePathResolver == null)
            {
                throw new ArgumentNullException(nameof(packagePathResolver));
            }

            if (packageExtractionContext == null)
            {
                throw new ArgumentNullException(nameof(packageExtractionContext));
            }

            var packageSaveMode = packageExtractionContext.PackageSaveMode;
            var filesAdded = new List<string>();
            var nupkgStartPosition = packageStream.Position;

            var packageExtractionTelemetryEvent = new PackageExtractionTelemetryEvent(packageExtractionContext.PackageSaveMode, NuGetOperationStatus.Failed, ExtractionSource.NuGetFolderProject);
            using (var telemetry = TelemetryActivity.Create(parentId, packageExtractionTelemetryEvent))
            {
                using (var packageReader = new PackageArchiveReader(packageStream, leaveStreamOpen: true))
                {
                    var packageIdentityFromNuspec = await packageReader.GetIdentityAsync(CancellationToken.None);
                    packageExtractionTelemetryEvent.LogPackageIdentity(packageIdentityFromNuspec);

                    var installPath = packagePathResolver.GetInstallPath(packageIdentityFromNuspec);
                    var packageDirectoryInfo = Directory.CreateDirectory(installPath);
                    var packageDirectory = packageDirectoryInfo.FullName;

                    telemetry.StartIntervalMeasure();

                    await VerifyPackageSignatureAsync(
                        source,
                        telemetry.OperationId,
                        packageIdentityFromNuspec,
                        packageExtractionContext,
                        packageReader,
                        token);

                    telemetry.EndIntervalMeasure(PackagingConstants.PackageVerifyDurationName);

                    var packageFiles = await packageReader.GetPackageFilesAsync(packageSaveMode, token);

                    if ((packageSaveMode & PackageSaveMode.Nuspec) == PackageSaveMode.Nuspec)
                    {
                        var sourceNuspecFile = packageFiles.Single(p => PackageHelper.IsManifest(p));

                        var targetNuspecPath = Path.Combine(
                            packageDirectory,
                            packagePathResolver.GetManifestFileName(packageIdentityFromNuspec));

                        // Extract the .nuspec file with a well known file name.
                        filesAdded.Add(packageReader.ExtractFile(
                            sourceNuspecFile,
                            targetNuspecPath,
                            packageExtractionContext.Logger));

                        packageFiles = packageFiles.Except(new[] { sourceNuspecFile });
                    }

                    var packageFileExtractor = new PackageFileExtractor(packageFiles, packageExtractionContext.XmlDocFileSaveMode);

                    filesAdded.AddRange(await packageReader.CopyFilesAsync(
                        packageDirectory,
                        packageFiles,
                        packageFileExtractor.ExtractPackageFile,
                        packageExtractionContext.Logger,
                        token));

                    if ((packageSaveMode & PackageSaveMode.Nupkg) == PackageSaveMode.Nupkg)
                    {
                        // During package extraction, nupkg is the last file to be created
                        // Since all the packages are already created, the package stream is likely positioned at its end
                        // Reset it to the nupkgStartPosition
                        packageStream.Seek(nupkgStartPosition, SeekOrigin.Begin);

                        var nupkgFilePath = Path.Combine(
                            packageDirectory,
                            packagePathResolver.GetPackageFileName(packageIdentityFromNuspec));

                        filesAdded.Add(packageStream.CopyToFile(nupkgFilePath));
                    }

                    // Now, copy satellite files unless requested to not copy them
                    if (packageExtractionContext.CopySatelliteFiles)
                    {
                        filesAdded.AddRange(await CopySatelliteFilesAsync(
                            packageReader,
                            packagePathResolver,
                            packageSaveMode,
                            packageExtractionContext,
                            token));
                    }

                    packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.Succeeded);
                }

                return filesAdded;
            }
        }

        /// <summary>
        /// Extracts a package, using the packages.config directory layout
        /// </summary>
        /// <remarks>For PackageReference directory layout, use <see cref="PackageExtractor.InstallFromSourceAsync(string, PackageIdentity, Func{Stream, Task}, VersionFolderPathResolver, PackageExtractionContext, CancellationToken, Guid)"/></remarks>
        /// <param name="source">The source from which the package was downloaded</param>
        /// <param name="packageReader">A <see cref="PackageReaderBase"/> with the package opened.</param>
        /// <param name="packageStream">A <see cref="Stream"/> for the nupkg file that is being extracted.</param>
        /// <param name="packagePathResolver">The V2 (packages.config) <see cref="PackagePathResolver"/>.</param>
        /// <param name="packageExtractionContext">The <see cref="PackageExtractionContext"/> with settings for how the extraction should be configured.</param>
        /// <param name="token">A cancellation token.</param>
        /// <param name="parentId">Telemetry parent ID.</param>
        /// <returns>A collection of files that were extracted.</returns>
        /// <exception cref="ArgumentNullException">If packageReader, packagePathResolver, or packageExtractionContext are null.</exception>
        /// <exception cref="ArgumentException">If packagestream is not seekable.</exception>
        /// <exception cref="SignatureException">If the package signature couldn't be validated. See exception message for more details.</exception>
        public static async Task<IEnumerable<string>> ExtractPackageAsync(
            string source,
            PackageReaderBase packageReader,
            Stream packageStream,
            PackagePathResolver packagePathResolver,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token,
            Guid parentId = default(Guid))
        {
            if (packageStream == null)
            {
                throw new ArgumentNullException(nameof(packageStream));
            }

            if (packagePathResolver == null)
            {
                throw new ArgumentNullException(nameof(packagePathResolver));
            }

            if (packageExtractionContext == null)
            {
                throw new ArgumentNullException(nameof(packageExtractionContext));
            }

            var packageSaveMode = packageExtractionContext.PackageSaveMode;
            var filesAdded = new List<string>();

            var packageExtractionTelemetryEvent = new PackageExtractionTelemetryEvent(packageExtractionContext.PackageSaveMode, NuGetOperationStatus.Failed, ExtractionSource.NuGetFolderProject);
            using (var telemetry = TelemetryActivity.Create(parentId, packageExtractionTelemetryEvent))
            {
                var packageIdentityFromNuspec = await packageReader.GetIdentityAsync(token);
                packageExtractionTelemetryEvent.LogPackageIdentity(packageIdentityFromNuspec);

                telemetry.StartIntervalMeasure();

                await VerifyPackageSignatureAsync(
                    source,
                    telemetry.OperationId,
                    packageIdentityFromNuspec,
                    packageExtractionContext,
                    packageReader,
                    token);

                telemetry.EndIntervalMeasure(PackagingConstants.PackageVerifyDurationName);

                var packageDirectoryInfo = Directory.CreateDirectory(packagePathResolver.GetInstallPath(packageIdentityFromNuspec));
                var packageDirectory = packageDirectoryInfo.FullName;

                var packageFiles = await packageReader.GetPackageFilesAsync(packageSaveMode, token);
                var packageFileExtractor = new PackageFileExtractor(packageFiles, packageExtractionContext.XmlDocFileSaveMode);
                filesAdded.AddRange(await packageReader.CopyFilesAsync(
                    packageDirectory,
                    packageFiles,
                    packageFileExtractor.ExtractPackageFile,
                    packageExtractionContext.Logger,
                    token));

                var nupkgFilePath = Path.Combine(packageDirectory, packagePathResolver.GetPackageFileName(packageIdentityFromNuspec));
                if (packageSaveMode.HasFlag(PackageSaveMode.Nupkg))
                {
                    // During package extraction, nupkg is the last file to be created
                    // Since all the packages are already created, the package stream is likely positioned at its end
                    // Reset it to the nupkgStartPosition
                    if (packageStream.Position != 0)
                    {
                        if (!packageStream.CanSeek)
                        {
                            throw new ArgumentException(Strings.PackageStreamShouldBeSeekable);
                        }

                        packageStream.Position = 0;
                    }

                    filesAdded.Add(packageStream.CopyToFile(nupkgFilePath));
                }

                // Now, copy satellite files unless requested to not copy them
                if (packageExtractionContext.CopySatelliteFiles)
                {
                    filesAdded.AddRange(await CopySatelliteFilesAsync(
                        packageReader,
                        packagePathResolver,
                        packageSaveMode,
                        packageExtractionContext,
                        token));
                }

                packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.Succeeded);
                return filesAdded;
            }
        }

        /// <summary>
        /// Extracts a package, using the packages.config directory layout
        /// </summary>
        /// <remarks>For PackageReference directory layout, use <see cref="PackageExtractor.InstallFromSourceAsync(string, PackageIdentity, Func{Stream, Task}, VersionFolderPathResolver, PackageExtractionContext, CancellationToken, Guid)"/></remarks>
        /// <param name="source">The source from which the package was downloaded</param>
        /// <param name="packageReader">A <see cref="PackageReaderBase"/> with the package opened.</param>
        /// <param name="packagePathResolver">The V2 (packages.config) <see cref="PackagePathResolver"/>.</param>
        /// <param name="packageExtractionContext">The <see cref="PackageExtractionContext"/> with settings for how the extraction should be configured.</param>
        /// <param name="token">A cancellation token.</param>
        /// <param name="parentId">Telemetry parent ID.</param>
        /// <returns>A collection of files that were extracted.</returns>
        /// <exception cref="ArgumentNullException">If packageReader, packagePathResolver, or packageExtractionContext are null.</exception>
        /// <exception cref="SignatureException">If the package signature couldn't be validated. See exception message for more details.</exception>
        public static async Task<IEnumerable<string>> ExtractPackageAsync(
            string source,
            PackageReaderBase packageReader,
            PackagePathResolver packagePathResolver,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token,
            Guid parentId = default(Guid))
        {
            if (packageReader == null)
            {
                throw new ArgumentNullException(nameof(packageReader));
            }

            if (packagePathResolver == null)
            {
                throw new ArgumentNullException(nameof(packagePathResolver));
            }

            if (packageExtractionContext == null)
            {
                throw new ArgumentNullException(nameof(packageExtractionContext));
            }

            token.ThrowIfCancellationRequested();

            var packageSaveMode = packageExtractionContext.PackageSaveMode;
            var filesAdded = new List<string>();

            var packageExtractionTelemetryEvent = new PackageExtractionTelemetryEvent(packageExtractionContext.PackageSaveMode, NuGetOperationStatus.Failed, ExtractionSource.NuGetFolderProject);
            using (var telemetry = TelemetryActivity.Create(parentId, packageExtractionTelemetryEvent))
            {
                var packageIdentityFromNuspec = await packageReader.GetIdentityAsync(token);
                packageExtractionTelemetryEvent.LogPackageIdentity(packageIdentityFromNuspec);

                telemetry.StartIntervalMeasure();

                await VerifyPackageSignatureAsync(
                    source,
                    telemetry.OperationId,
                    packageIdentityFromNuspec,
                    packageExtractionContext,
                    packageReader,
                    token);

                telemetry.EndIntervalMeasure(PackagingConstants.PackageVerifyDurationName);

                var packageDirectoryInfo = Directory.CreateDirectory(packagePathResolver.GetInstallPath(packageIdentityFromNuspec));
                var packageDirectory = packageDirectoryInfo.FullName;

                var packageFiles = await packageReader.GetPackageFilesAsync(packageSaveMode, token);
                var packageFileExtractor = new PackageFileExtractor(packageFiles, packageExtractionContext.XmlDocFileSaveMode);

                filesAdded.AddRange(await packageReader.CopyFilesAsync(
                    packageDirectory,
                    packageFiles,
                    packageFileExtractor.ExtractPackageFile,
                    packageExtractionContext.Logger,
                    token));

                if (packageSaveMode.HasFlag(PackageSaveMode.Nupkg))
                {
                    var nupkgFilePath = Path.Combine(packageDirectory, packagePathResolver.GetPackageFileName(packageIdentityFromNuspec));
                    var filePath = await packageReader.CopyNupkgAsync(nupkgFilePath, token);

                    if (!string.IsNullOrEmpty(filePath))
                    {
                        filesAdded.Add(filePath);
                    }
                }

                // Now, copy satellite files unless requested to not copy them
                if (packageExtractionContext.CopySatelliteFiles)
                {
                    filesAdded.AddRange(await CopySatelliteFilesAsync(
                        packageReader,
                        packagePathResolver,
                        packageSaveMode,
                        packageExtractionContext,
                        token));
                }

                packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.Succeeded);
                return filesAdded;
            }
        }

        /// <summary>
        /// Uses a copy function to install a package to a global packages directory with the PackageReference directory layout.
        /// </summary>
        /// <param name="source">The source where the nupkg was downloaded from.</param>
        /// <param name="packageIdentity">The package id + version being installed.</param>
        /// <param name="copyToAsync">
        /// A function which should copy the package to the provided destination stream.
        /// </param>
        /// <param name="versionFolderPathResolver">The path resolver for PackageReference layout.</param>
        /// <param name="packageExtractionContext">
        /// The version folder path context, which encapsulates all of the parameters to observe
        /// while installing the package.
        /// </param>
        /// <param name="token">The cancellation token.</param>
        /// <param name="parentId">The telemetry parent ID.</param>
        /// <returns>
        /// True if the package was installed. False if the package already exists and therefore
        /// resulted in no copy operation.
        /// </returns>
        /// <exception cref="ArgumentNullException">If copyToAsync or packageExtractionContext is null.</exception>
        /// <exception cref="SignatureException">If the package signature couldn't be validated. See exception message for more details.</exception>
        public static async Task<bool> InstallFromSourceAsync(
            string source,
            PackageIdentity packageIdentity,
            Func<Stream, Task> copyToAsync,
            VersionFolderPathResolver versionFolderPathResolver,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token,
            Guid parentId = default(Guid))
        {
            if (copyToAsync == null)
            {
                throw new ArgumentNullException(nameof(copyToAsync));
            }

            if (packageExtractionContext == null)
            {
                throw new ArgumentNullException(nameof(packageExtractionContext));
            }

            var logger = packageExtractionContext.Logger;

            var packageExtractionTelemetryEvent = new PackageExtractionTelemetryEvent(packageExtractionContext.PackageSaveMode, NuGetOperationStatus.Failed, ExtractionSource.DownloadResource, packageIdentity);
            using (var telemetry = TelemetryActivity.Create(parentId, packageExtractionTelemetryEvent))
            {

                var targetPath = versionFolderPathResolver.GetInstallPath(packageIdentity.Id, packageIdentity.Version);
                var targetNuspec = versionFolderPathResolver.GetManifestFilePath(packageIdentity.Id, packageIdentity.Version);
                var targetNupkg = versionFolderPathResolver.GetPackageFilePath(packageIdentity.Id, packageIdentity.Version);
                var hashPath = versionFolderPathResolver.GetHashPath(packageIdentity.Id, packageIdentity.Version);
                var nupkgMetadataFilePath = versionFolderPathResolver.GetNupkgMetadataPath(packageIdentity.Id, packageIdentity.Version);

                logger.LogVerbose(
                    $"Acquiring lock for the installation of {packageIdentity.Id} {packageIdentity.Version}");

                // Acquire the lock on a nukpg before we extract it to prevent the race condition when multiple
                // processes are extracting to the same destination simultaneously
                return await ConcurrencyUtilities.ExecuteWithFileLockedAsync(targetNupkg,
                    action: async cancellationToken =>
                    {
                        // If this is the first process trying to install the target nupkg, go ahead
                        // After this process successfully installs the package, all other processes
                        // waiting on this lock don't need to install it again.
                        if (!File.Exists(nupkgMetadataFilePath))
                        {
                            logger.LogVerbose(
                                $"Acquired lock for the installation of {packageIdentity.Id} {packageIdentity.Version}");

                            cancellationToken.ThrowIfCancellationRequested();

                            // We do not stop the package extraction after this point
                            // based on CancellationToken, but things can still be stopped if the process is killed.
                            if (Directory.Exists(targetPath))
                            {
                                // If we had a broken restore, clean out the files first
                                var info = new DirectoryInfo(targetPath);

                                foreach (var file in info.GetFiles())
                                {
                                    file.Delete();
                                }

                                foreach (var dir in info.GetDirectories())
                                {
                                    dir.Delete(true);
                                }
                            }
                            else
                            {
                                Directory.CreateDirectory(targetPath);
                            }

                            var targetTempNupkg = Path.Combine(targetPath, Path.GetRandomFileName());
                            var tempHashPath = Path.Combine(targetPath, Path.GetRandomFileName());
                            var tempNupkgMetadataPath = Path.Combine(targetPath, Path.GetRandomFileName());
                            var packageSaveMode = packageExtractionContext.PackageSaveMode;
                            string contentHash;

                            try
                            {
                                // Extract the nupkg
                                using (var nupkgStream = new FileStream(
                                    targetTempNupkg,
                                    FileMode.Create,
                                    FileAccess.ReadWrite,
                                    FileShare.ReadWrite | FileShare.Delete,
                                    bufferSize: 4096))
                                {
                                    await copyToAsync(nupkgStream);
                                    nupkgStream.Seek(0, SeekOrigin.Begin);

                                    using (var packageReader = new PackageArchiveReader(nupkgStream))
                                    {
                                        ValidateExpectedPackage(packageIdentity, packageReader);

                                        if (packageSaveMode.HasFlag(PackageSaveMode.Nuspec) || packageSaveMode.HasFlag(PackageSaveMode.Files))
                                        {
                                            telemetry.StartIntervalMeasure();

                                            await VerifyPackageSignatureAsync(
                                                source,
                                                telemetry.OperationId,
                                                packageIdentity,
                                                packageExtractionContext,
                                                packageReader,
                                                token);

                                            telemetry.EndIntervalMeasure(PackagingConstants.PackageVerifyDurationName);
                                        }

                                        var nuspecFile = packageReader.GetNuspecFile();
                                        if ((packageSaveMode & PackageSaveMode.Nuspec) == PackageSaveMode.Nuspec)
                                        {
                                            packageReader.ExtractFile(nuspecFile, targetNuspec, logger);
                                        }

                                        if ((packageSaveMode & PackageSaveMode.Files) == PackageSaveMode.Files)
                                        {
                                            var hashFileName = Path.GetFileName(hashPath);
                                            var nupkgMetadataFileName = Path.GetFileName(nupkgMetadataFilePath);
                                            var packageFiles = packageReader.GetFiles()
                                                .Where(file => ShouldInclude(file, hashFileName, nupkgMetadataFileName));
                                            var packageFileExtractor = new PackageFileExtractor(
                                                packageFiles,
                                                packageExtractionContext.XmlDocFileSaveMode);
                                            packageReader.CopyFiles(
                                                targetPath,
                                                packageFiles,
                                                packageFileExtractor.ExtractPackageFile,
                                                logger,
                                                token);
                                        }

                                        nupkgStream.Position = 0;
                                        var packageHash = Convert.ToBase64String(new CryptoHashProvider("SHA512").CalculateHash(nupkgStream));

                                        File.WriteAllText(tempHashPath, packageHash);

                                        // get hash for the unsigned content of package
                                        contentHash = packageReader.GetContentHash(cancellationToken, GetUnsignedPackageHash: () => packageHash);

                                        // write the new hash file
                                        var hashFile = new NupkgMetadataFile()
                                        {
                                            ContentHash = contentHash,
                                            Source = source
                                        };

                                        NupkgMetadataFileFormat.Write(tempNupkgMetadataPath, hashFile);
                                    }
                                }
                            }
                            catch (SignatureException)
                            {
                                try
                                {
                                    DeleteTargetAndTempPaths(targetPath, targetTempNupkg);
                                }
                                catch (IOException ex)
                                {
                                    logger.LogWarning(string.Format(
                                        CultureInfo.CurrentCulture,
                                        Strings.ErrorUnableToDeleteFile,
                                        targetTempNupkg,
                                        ex.Message));
                                }

                                throw;
                            }

                            // Now rename the tmp file
                            if ((packageExtractionContext.PackageSaveMode & PackageSaveMode.Nupkg) ==
                                    PackageSaveMode.Nupkg)
                            {
                                File.Move(targetTempNupkg, targetNupkg);
                            }
                            else
                            {
                                try
                                {
                                    File.Delete(targetTempNupkg);
                                }
                                catch (IOException ex)
                                {
                                    logger.LogWarning(string.Format(
                                        CultureInfo.CurrentCulture,
                                        Strings.ErrorUnableToDeleteFile,
                                        targetTempNupkg,
                                        ex.Message));
                                }
                            }

                            // Note: PackageRepository relies on the hash file being written out as the
                            // final operation as part of a package install to assume a package was fully installed.
                            // Rename the tmp hash file
                            File.Move(tempHashPath, hashPath);

                            File.Move(tempNupkgMetadataPath, nupkgMetadataFilePath);

                            logger.LogInformation(StringFormatter.Log_InstalledPackage(packageIdentity.Id, packageIdentity.Version.OriginalVersion!, source, contentHash, targetPath));

                            packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.Succeeded);
                            return true;
                        }
                        else
                        {
                            logger.LogVerbose("Lock not required - Package already installed "
                                                + $"{packageIdentity.Id} {packageIdentity.Version}");

                            packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.NoOp);
                            return false;
                        }
                    },
                    token: token);
            }
        }

        private static void ValidateExpectedPackage(PackageIdentity packageIdentity, PackageArchiveReader packageReader)
        {
            PackageIdentity actualIdentity = packageReader.GetIdentity();
            if (!PackageIdentityComparer.Default.Equals(packageIdentity, actualIdentity))
            {
                string message = string.Format(
                    CultureInfo.InvariantCulture,
                    Strings.ErrorPackageIdentityDoesNotMatch,
                    packageIdentity.Id,
                    packageIdentity.Version,
                    actualIdentity.Id,
                    actualIdentity.Version);
                throw new PackagingException(message);
            }
        }

        /// <summary>
        /// Delete the target directory path and the temp nupkg path in case of a failed extraction.
        /// </summary>
        private static void DeleteTargetAndTempPaths(string targetPath, string targetTempNupkg)
        {
            if (File.Exists(targetTempNupkg))
            {
                File.Delete(targetTempNupkg);
            }

            if (Directory.Exists(targetPath))
            {
                var parent = Directory.GetParent(targetPath);
                Directory.Delete(targetPath);

                // delete the parent directory if it is empty
                if (parent != null &&
                    Directory.Exists(parent.FullName) &&
                    !parent.GetFiles().Any() &&
                    !parent.GetDirectories().Any())
                {
                    Directory.Delete(parent.FullName);
                }
            }
        }

        /// <summary>
        /// Uses a copy function to install a package to a global packages directory with the PackageReference directory layout.
        /// </summary>
        /// <param name="packageIdentity">The package id + version being installed.</param>
        /// <param name="packageDownloader">
        /// A <see cref="IPackageDownloader"/> to download the nupkg.
        /// </param>
        /// <param name="versionFolderPathResolver">The path resolver for PackageReference layout.</param>
        /// <param name="packageExtractionContext">
        /// The version folder path context, which encapsulates all of the parameters to observe
        /// while installing the package.
        /// </param>
        /// <param name="token">The cancellation token.</param>
        /// <param name="parentId">The telemetry parent ID.</param>
        /// <returns>
        /// True if the package was installed. False if the package already exists and therefore
        /// resulted in no copy operation.
        /// </returns>
        /// <exception cref="ArgumentNullException">If packageDownlaoder or packageExtractionContext is null.</exception>
        /// <exception cref="SignatureException">If the package signature couldn't be validated. See exception message for more details.</exception>
        public static async Task<bool> InstallFromSourceAsync(
            PackageIdentity packageIdentity,
            IPackageDownloader packageDownloader,
            VersionFolderPathResolver versionFolderPathResolver,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token,
            Guid parentId = default(Guid))
        {
            if (packageDownloader == null)
            {
                throw new ArgumentNullException(nameof(packageDownloader));
            }

            if (packageExtractionContext == null)
            {
                throw new ArgumentNullException(nameof(packageExtractionContext));
            }

            var logger = packageExtractionContext.Logger;

            var packageExtractionTelemetryEvent = new PackageExtractionTelemetryEvent(packageExtractionContext.PackageSaveMode, NuGetOperationStatus.Failed, ExtractionSource.RestoreCommand, packageIdentity);
            using (var telemetry = TelemetryActivity.Create(parentId, packageExtractionTelemetryEvent))
            {
                var targetPath = versionFolderPathResolver.GetInstallPath(packageIdentity.Id, packageIdentity.Version);
                var targetNuspec = versionFolderPathResolver.GetManifestFilePath(packageIdentity.Id, packageIdentity.Version);
                var targetNupkg = versionFolderPathResolver.GetPackageFilePath(packageIdentity.Id, packageIdentity.Version);
                var hashPath = versionFolderPathResolver.GetHashPath(packageIdentity.Id, packageIdentity.Version);
                var nupkgMetadataFilePath = versionFolderPathResolver.GetNupkgMetadataPath(packageIdentity.Id, packageIdentity.Version);

                logger.LogVerbose(
                    $"Acquiring lock for the installation of {packageIdentity.Id} {packageIdentity.Version}");

                // Acquire the lock on a nukpg before we extract it to prevent the race condition when multiple
                // processes are extracting to the same destination simultaneously
                return await ConcurrencyUtilities.ExecuteWithFileLockedAsync(targetNupkg,
                    action: async cancellationToken =>
                    {
                        // If this is the first process trying to install the target nupkg, go ahead
                        // After this process successfully installs the package, all other processes
                        // waiting on this lock don't need to install it again.
                        if (!File.Exists(nupkgMetadataFilePath))
                        {
                            logger.LogVerbose(
                                $"Acquired lock for the installation of {packageIdentity.Id} {packageIdentity.Version}");

                            cancellationToken.ThrowIfCancellationRequested();

                            // We do not stop the package extraction after this point
                            // based on CancellationToken, but things can still be stopped if the process is killed.
                            if (Directory.Exists(targetPath))
                            {
                                // If we had a broken restore, clean out the files first
                                var info = new DirectoryInfo(targetPath);

                                foreach (var file in info.GetFiles())
                                {
                                    file.Delete();
                                }

                                foreach (var dir in info.GetDirectories())
                                {
                                    dir.Delete(true);
                                }
                            }
                            else
                            {
                                Directory.CreateDirectory(targetPath);
                            }

                            var targetTempNupkg = Path.Combine(targetPath, Path.GetRandomFileName());
                            var tempHashPath = Path.Combine(targetPath, Path.GetRandomFileName());
                            var tempNupkgMetadataFilePath = Path.Combine(targetPath, Path.GetRandomFileName());
                            var packageSaveMode = packageExtractionContext.PackageSaveMode;

                            // Extract the nupkg
                            var copiedNupkg = await packageDownloader.CopyNupkgFileToAsync(targetTempNupkg, cancellationToken);

                            if (packageSaveMode.HasFlag(PackageSaveMode.Nuspec) || packageSaveMode.HasFlag(PackageSaveMode.Files))
                            {
                                try
                                {
                                    telemetry.StartIntervalMeasure();

                                    await VerifyPackageSignatureAsync(
                                        packageDownloader.Source,
                                        telemetry.OperationId,
                                        packageIdentity,
                                        packageExtractionContext,
                                        packageDownloader.SignedPackageReader,
                                        token);

                                    telemetry.EndIntervalMeasure(PackagingConstants.PackageVerifyDurationName);

                                }
                                catch (SignatureException)
                                {
                                    try
                                    {
                                        // Dispose of it now because it is holding a lock on the temporary .nupkg file.
                                        packageDownloader.Dispose();

                                        DeleteTargetAndTempPaths(targetPath, targetTempNupkg);
                                    }
                                    catch (IOException ex)
                                    {
                                        logger.LogWarning(string.Format(
                                            CultureInfo.CurrentCulture,
                                            Strings.ErrorUnableToDeleteFile,
                                            targetTempNupkg,
                                            ex.Message));
                                    }

                                    throw;
                                }
                            }

                            if (packageSaveMode.HasFlag(PackageSaveMode.Nuspec))
                            {
                                var nuspecFileNameFromReader = await packageDownloader.CoreReader.GetNuspecFileAsync(cancellationToken);
                                var packageFiles = new[] { nuspecFileNameFromReader };
                                var packageFileExtractor = new PackageFileExtractor(
                                    packageFiles,
                                    XmlDocFileSaveMode.None);
                                var packageDirectoryPath = Path.GetDirectoryName(targetNuspec)!;

                                var extractedNuspecFilePath = (await packageDownloader.CoreReader.CopyFilesAsync(
                                        packageDirectoryPath,
                                        packageFiles,
                                        packageFileExtractor.ExtractPackageFile,
                                        logger,
                                        cancellationToken))
                                    .SingleOrDefault();

                                // CopyFilesAsync(...) just extracts files to a directory.
                                // We may have to fix up the casing of the .nuspec file name.
                                if (!string.IsNullOrEmpty(extractedNuspecFilePath))
                                {
                                    if (PathUtility.IsFileSystemCaseInsensitive)
                                    {
                                        var nuspecFileName = Path.GetFileName(targetNuspec);
                                        var actualNuspecFileName = Path.GetFileName(extractedNuspecFilePath);

                                        if (!string.Equals(nuspecFileName, actualNuspecFileName, StringComparison.Ordinal))
                                        {
                                            var tempNuspecFilePath = Path.Combine(packageDirectoryPath, Path.GetRandomFileName());

                                            File.Move(extractedNuspecFilePath, tempNuspecFilePath);
                                            File.Move(tempNuspecFilePath, targetNuspec);
                                        }
                                    }
                                    else if (!File.Exists(targetNuspec))
                                    {
                                        File.Move(extractedNuspecFilePath, targetNuspec);
                                    }
                                }
                            }

                            if (packageSaveMode.HasFlag(PackageSaveMode.Files))
                            {
                                var hashFileName = Path.GetFileName(hashPath);
                                var nupkgMetadataFileName = Path.GetFileName(nupkgMetadataFilePath);
                                var packageFiles = (await packageDownloader.CoreReader.GetFilesAsync(cancellationToken))
                                    .Where(file => ShouldInclude(file, hashFileName, nupkgMetadataFileName));
                                var packageFileExtractor = new PackageFileExtractor(
                                    packageFiles,
                                    packageExtractionContext.XmlDocFileSaveMode);
                                await packageDownloader.CoreReader.CopyFilesAsync(
                                    targetPath,
                                    packageFiles,
                                    packageFileExtractor.ExtractPackageFile,
                                    logger,
                                    token);
                            }

                            var packageHash = await packageDownloader.GetPackageHashAsync("SHA512", cancellationToken);

                            File.WriteAllText(tempHashPath, packageHash);

                            // get hash for the unsigned content of package
                            var contentHash = packageDownloader.SignedPackageReader.GetContentHash(cancellationToken, GetUnsignedPackageHash: () => packageHash);

                            // write the new hash file
                            var hashFile = new NupkgMetadataFile()
                            {
                                ContentHash = contentHash,
                                Source = packageDownloader.Source
                            };

                            NupkgMetadataFileFormat.Write(tempNupkgMetadataFilePath, hashFile);

                            // Now rename the tmp file
                            if (packageExtractionContext.PackageSaveMode.HasFlag(PackageSaveMode.Nupkg))
                            {
                                if (copiedNupkg)
                                {
                                    // Dispose of it now because it is holding a lock on the temporary .nupkg file.
                                    packageDownloader.Dispose();

                                    File.Move(targetTempNupkg, targetNupkg);
                                }
                            }
                            else
                            {
                                try
                                {
                                    File.Delete(targetTempNupkg);
                                }
                                catch (IOException ex)
                                {
                                    logger.LogWarning(string.Format(
                                        CultureInfo.CurrentCulture,
                                        Strings.ErrorUnableToDeleteFile,
                                        targetTempNupkg,
                                        ex.Message));
                                }
                            }

                            // Note: PackageRepository relies on the hash file being written out as the
                            // final operation as part of a package install to assume a package was fully installed.
                            // Rename the tmp hash file
                            File.Move(tempHashPath, hashPath);

                            File.Move(tempNupkgMetadataFilePath, nupkgMetadataFilePath);

                            logger.LogInformation(StringFormatter.Log_InstalledPackage(packageIdentity.Id, packageIdentity.Version.OriginalVersion!, packageDownloader.Source, contentHash, targetPath));

                            packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.Succeeded);
                            return true;
                        }
                        else
                        {
                            logger.LogVerbose("Lock not required - Package already installed "
                                                + $"{packageIdentity.Id} {packageIdentity.Version}");

                            packageExtractionTelemetryEvent.SetResult(NuGetOperationStatus.NoOp);
                            return false;
                        }
                    },
                    token: token);
            }
        }

        private static bool ShouldInclude(
            string fullName,
            string hashFileName,
            string nupkgMetadataFileName)
        {
            // Not all the files from a zip file are needed
            // So, files such as '.rels' and '[Content_Types].xml' are not extracted
            var fileName = Path.GetFileName(fullName);
            if (fileName != null)
            {
                if (fileName == ".rels")
                {
                    return false;
                }
                if (fileName == "[Content_Types].xml")
                {
                    return false;
                }
            }

            var extension = Path.GetExtension(fullName);
            if (extension == ".psmdcp")
            {
                return false;
            }

            if (string.Equals(fullName, hashFileName, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            // Skip nupkgs and nuspec files found in the root, the valid ones are already extracted
            if (PackageHelper.IsRoot(fullName)
                && (PackageHelper.IsNuspec(fullName)
                    || fullName.EndsWith(PackagingCoreConstants.NupkgExtension, StringComparison.OrdinalIgnoreCase)))
            {
                return false;
            }

            // skip .nupkg.metadata file
            if (string.Equals(fullName, nupkgMetadataFileName, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            return true;
        }

        public static async Task<IEnumerable<string>> CopySatelliteFilesAsync(
            PackageIdentity packageIdentity,
            PackagePathResolver packagePathResolver,
            PackageSaveMode packageSaveMode,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token)
        {
            if (packageIdentity == null)
            {
                throw new ArgumentNullException(nameof(packageIdentity));
            }

            if (packagePathResolver == null)
            {
                throw new ArgumentNullException(nameof(packagePathResolver));
            }

            var satelliteFilesCopied = Enumerable.Empty<string>();

            var nupkgFilePath = packagePathResolver.GetInstalledPackageFilePath(packageIdentity);
            if (File.Exists(nupkgFilePath))
            {
                using (var packageReader = new PackageArchiveReader(nupkgFilePath!))
                {
                    ValidateExpectedPackage(packageIdentity, packageReader);
                    return await CopySatelliteFilesAsync(
                        packageReader,
                        packagePathResolver,
                        packageSaveMode,
                        packageExtractionContext,
                        token);
                }
            }

            return satelliteFilesCopied;
        }

        private static async Task<IEnumerable<string>> CopySatelliteFilesAsync(
            PackageReaderBase packageReader,
            PackagePathResolver packagePathResolver,
            PackageSaveMode packageSaveMode,
            PackageExtractionContext packageExtractionContext,
            CancellationToken token)
        {
            if (packageReader == null)
            {
                throw new ArgumentNullException(nameof(packageReader));
            }

            if (packagePathResolver == null)
            {
                throw new ArgumentNullException(nameof(packagePathResolver));
            }

            if (packageExtractionContext == null)
            {
                throw new ArgumentNullException(nameof(packageExtractionContext));
            }

            var satelliteFilesCopied = Enumerable.Empty<string>();

            var result = await PackageHelper.GetSatelliteFilesAsync(packageReader, packagePathResolver, token);

            var runtimePackageDirectory = result.Item1;
            var satelliteFiles = result.Item2
                .Where(file => PackageHelper.IsPackageFile(file, packageSaveMode))
                .ToList();

            if (satelliteFiles.Count > 0)
            {
                var packageFileExtractor = new PackageFileExtractor(satelliteFiles, packageExtractionContext.XmlDocFileSaveMode);

                // Now, add all the satellite files collected from the package to the runtime package folder(s)
                satelliteFilesCopied = await packageReader.CopyFilesAsync(
                    runtimePackageDirectory!,
                    satelliteFiles,
                    packageFileExtractor.ExtractPackageFile,
                    packageExtractionContext.Logger,
                    token);
            }

            return satelliteFilesCopied;
        }

        private static async Task VerifyPackageSignatureAsync(
            string source,
            Guid parentId,
            PackageIdentity package,
            PackageExtractionContext packageExtractionContext,
            ISignedPackageReader signedPackageReader,
            CancellationToken token)
        {
            if (packageExtractionContext.ClientPolicyContext != null)
            {
                var clientPolicyContext = packageExtractionContext.ClientPolicyContext;
                var repositorySignatureInfo = GetRepositorySignatureInfo(source);

                var verificationSettings = RepositorySignatureInfoUtility.GetSignedPackageVerifierSettings(
                    repositorySignatureInfo,
                    clientPolicyContext.VerifierSettings);

                if (!signedPackageReader.CanVerifySignedPackages(verificationSettings))
                {
                    return;
                }

                IPackageSignatureVerifier? signedPackageVerifier = null;
                if (packageExtractionContext.SignedPackageVerifier != null)
                {
                    // If we have a verifier in the extraction context it means is a test that wants to override it, so we take it blindly.
                    signedPackageVerifier = packageExtractionContext.SignedPackageVerifier;
                }
                else
                {
                    var verificationProviders = new List<ISignatureVerificationProvider>()
                    {
                        new IntegrityVerificationProvider(),
                    };

                    verificationProviders.Add(
                      new AllowListVerificationProvider(
                          clientPolicyContext.AllowList,
                          requireNonEmptyAllowList: clientPolicyContext.Policy == SignatureValidationMode.Require,
                          emptyListErrorMessage: Strings.Error_NoClientAllowList,
                          noMatchErrorMessage: Strings.Error_NoMatchingClientCertificate));

                    IEnumerable<KeyValuePair<string, HashAlgorithmName>>? allowUntrustedRootList = null;
                    if (clientPolicyContext.AllowList != null && clientPolicyContext.AllowList.Any())
                    {
                        allowUntrustedRootList = clientPolicyContext.AllowList
                            .Where(e => e.AllowUntrustedRoot)
                            .Select(e => new KeyValuePair<string, HashAlgorithmName>(e.Fingerprint, e.FingerprintAlgorithm));
                    }

                    verificationProviders.Add(new SignatureTrustAndValidityVerificationProvider(allowUntrustedRootList));

                    if (repositorySignatureInfo != null && repositorySignatureInfo.RepositoryCertificateInfos != null)
                    {
                        var repositoryAllowList = RepositorySignatureInfoUtility.GetRepositoryAllowList(repositorySignatureInfo.RepositoryCertificateInfos);

                        verificationProviders.Add(new AllowListVerificationProvider(
                            repositoryAllowList,
                            repositorySignatureInfo.AllRepositorySigned,
                            Strings.Error_NoRepoAllowList,
                            Strings.Error_NoMatchingRepositoryCertificate));
                    }

                    signedPackageVerifier = new PackageSignatureVerifier(verificationProviders);
                }

                var verifyResult = await signedPackageVerifier.VerifySignaturesAsync(
                       signedPackageReader,
                       verificationSettings,
                       token,
                       parentId);

                if (verifyResult.IsSigned || !verifyResult.IsValid)
                {
                    await LogPackageSignatureVerificationAsync(source, package, packageExtractionContext.Logger, verifyResult);

                    // Update errors and warnings
                    verifyResult.Results
                            .SelectMany(r => r.Issues)
                            .ForEach(e => UpdateSignatureLog(source, package, clientPolicyContext, e));

                    if (verifyResult.IsValid)
                    {
                        // log any warnings
                        var warnings = verifyResult.Results.SelectMany(r => r.GetWarningIssues());

                        foreach (var warning in warnings)
                        {
                            await packageExtractionContext.Logger.LogAsync(warning);
                        }

                        if (packageExtractionContext.Logger is ICollectorLogger collectorLogger)
                        {
                            // collectorLogger.Errors is a collection of errors and warnings, we just need to fail if there are errors.
                            if (collectorLogger.Errors.Any(e => e.Level >= LogLevel.Error))
                            {
                                // Send empty results since errors and warnings have already been logged
                                throw new SignatureException(results: Enumerable.Empty<PackageVerificationResult>().ToList(), package: package);
                            }
                        }
                    }
                    else
                    {
                        throw new SignatureException(verifyResult.Results, package);
                    }
                }
            }
        }

        private static void UpdateSignatureLog(string source, PackageIdentity package, ClientPolicyContext policyContext, SignatureLog message)
        {
            if (message.Code == NuGetLogCode.NU3004)
            {
                message.Message = policyContext.Policy == SignatureValidationMode.Require ? Strings.Error_RequireMode_UnsignedPackage : Strings.Error_RepositorySettings_UnsignedPackage;
            }

            message.LibraryId = package.Id;
            message.Message = string.Format(CultureInfo.CurrentCulture, Strings.ExtractionLog_InformationPrefix, package.Id, package.Version, source, message.Message);
        }

        private static async Task LogPackageSignatureVerificationAsync(
            string source,
            PackageIdentity package,
            ILogger logger,
            VerifySignaturesResult verifyResult)
        {
            await logger.LogAsync(
                LogLevel.Verbose,
                string.Format(CultureInfo.CurrentCulture, Strings.PackageSignatureVerificationLog, package, source, verifyResult.IsValid));
        }

        private static RepositorySignatureInfo? GetRepositorySignatureInfo(string? source)
        {
            RepositorySignatureInfo? repositorySignatureInfo = null;

            if (!string.IsNullOrEmpty(source))
            {
                RepositorySignatureInfoProvider.Instance.TryGetRepositorySignatureInfo(source!, out repositorySignatureInfo);
            }

            return repositorySignatureInfo;
        }
    }
}