File: Utility\LocalFolderUtility.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol)
// 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.Security;
using System.Threading;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

namespace NuGet.Protocol
{
    public static class LocalFolderUtility
    {
        // *.nupkg
        private static readonly string NupkgFilter = $"*{NuGetConstants.PackageExtension}";

        /// <summary>
        /// Retrieve a nupkg using the path.
        /// </summary>
        /// <param name="path">Nupkg path in uri form.</param>
        public static LocalPackageInfo? GetPackage(Uri path, ILogger log)
        {
            if (path == null)
            {
                throw new ArgumentNullException(nameof(path));
            }

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

            var file = GetAndVerifyFileInfo(path);

            return GetPackageFromNupkg(file);
        }

        /// <summary>
        /// Retrieve all packages from a folder and one level deep.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV2(string root, ILogger log)
        {
            return GetPackagesV2(root, log, CancellationToken.None);
        }

        /// <summary>
        /// Retrieve all packages from a folder and one level deep.
        /// </summary>
        /// <param name="root">Folder path</param>
        /// <param name="log">Logger</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV2(string root, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

            return GetPackagesFromNupkgs(GetNupkgsFromFlatFolder(root, log, cancellationToken), cancellationToken);
        }

        /// <summary>
        /// Retrieve all packages of an id from a v2 folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="id">Package id.</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV2(string root, string id, ILogger log)
        {
            return GetPackagesV2(root, id, log, CancellationToken.None);
        }

        /// <summary>
        /// Retrieve all packages of an id from a v2 folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="id">Package id.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV2(string root, string id, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            foreach (var package in GetPackagesFromNupkgs(GetNupkgsFromFlatFolder(root, id, log, cancellationToken), cancellationToken))
            {
                // Filter out any packages that were incorrectly identified
                // Ex: id: packageA.1 version: 1.0 -> packageA.1.1.0 -> packageA 1.1.0
                if (StringComparer.OrdinalIgnoreCase.Equals(id, package.Identity.Id))
                {
                    yield return package;
                }
            }

            yield break;
        }

        /// <summary>
        /// Retrieve all packages of an id from a v2 folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="id">Package id.</param>
        /// <param name="version">Package version.</param>
        public static LocalPackageInfo? GetPackageV2(string root, string id, NuGetVersion version, ILogger log)
        {
            return GetPackageV2(root, id, version, log, CancellationToken.None);
        }

        /// <summary>
        /// Retrieve all packages of an id from a v2 folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="id">Package id.</param>
        /// <param name="version">Package version.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static LocalPackageInfo? GetPackageV2(string root, string id, NuGetVersion version, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

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

            return GetPackageV2(root, new PackageIdentity(id, version), log, cancellationToken);
        }

        /// <summary>
        /// Retrieve all packages of an id from a v2 folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="identity">Package id and version.</param>
        public static LocalPackageInfo? GetPackageV2(string root, PackageIdentity identity, ILogger log)
        {
            return GetPackageV2(root, identity, log, CancellationToken.None);
        }

        /// <summary>
        /// Retrieve all packages of an id from a v2 folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="identity">Package id and version.</param>
        /// /// <param name="cancellationToken">Cancellation token</param>
        public static LocalPackageInfo? GetPackageV2(string root, PackageIdentity identity, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            // Verify the root path is a valid path.
            var rootDirInfo = GetAndVerifyRootDirectory(root);

            // Search directories starting with the top directory for any package matching the identity
            // If multiple packages are found in the same directory that match (ex: 1.0, 1.0.0.0)
            // then favor the exact non-normalized match. If no exact match is found take the first
            // using the file system sort order. This is to match the legacy nuget 2.8.x behavior.
            foreach (var directoryList in GetNupkgsFromFlatFolderChunked(rootDirInfo, log, cancellationToken))
            {
                LocalPackageInfo? fallbackMatch = null;

                // Check for any files that are in the form packageId.version.nupkg
                foreach (var file in directoryList.Where(file => IsPossiblePackageMatch(file, identity)))
                {
                    var package = GetPackageFromNupkg(file);

                    if (identity.Equals(package.Identity))
                    {
                        if (StringComparer.OrdinalIgnoreCase.Equals(
                            identity.Version.ToString(),
                            package.Identity.Version.ToString()))
                        {
                            // Take an exact match immediately
                            return package;
                        }
                        else if (fallbackMatch == null)
                        {
                            // This matches the identity, but there may be an exact match still
                            fallbackMatch = package;
                        }
                    }
                }

                if (fallbackMatch != null)
                {
                    // Use the fallback match if an exact match was not found
                    return fallbackMatch;
                }
            }

            // Not found
            return null;
        }

        /// <summary>
        /// Retrieve a package with an id and version from a packages.config packages folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesConfigFolderPackages(
            string root,
            ILogger log)
        {
            return GetPackagesConfigFolderPackages(root, log, CancellationToken.None);
        }

        /// <summary>
        /// Retrieve a package with an id and version from a packages.config packages folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesConfigFolderPackages(
            string root,
            ILogger log,
            CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

            var rootDirInfo = GetAndVerifyRootDirectory(root);

            // Find the matching nupkg for each sub directory.
            if (rootDirInfo.Exists)
            {
                foreach (var dir in GetDirectoriesSafe(rootDirInfo, log, cancellationToken))
                {
                    var package = GetPackagesConfigFolderPackage(dir, log);

                    // Ensure that the nupkg file exists
                    if (package != null)
                    {
                        yield return package;
                    }
                }
            }

            yield break;
        }

        /// <summary>
        /// Retrieve a package with an id and version from a packages.config packages folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesConfigFolderPackages(
            string root,
            string id,
            ILogger log)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            var rootDirInfo = GetAndVerifyRootDirectory(root);

            if (rootDirInfo.Exists)
            {
                var searchPattern = GetPackagesConfigFolderSearchPattern(id);

                foreach (var dir in GetDirectoriesSafe(rootDirInfo, searchPattern, SearchOption.TopDirectoryOnly, log, CancellationToken.None))
                {
                    // Check the id and version of the path, if the id matches and the version
                    // is valid this will be non-null;
                    var dirVersion = GetVersionFromIdVersionString(dir.Name, id);

                    if (dirVersion != null)
                    {
                        var package = GetPackagesConfigFolderPackage(dir, log);

                        if (package != null)
                        {
                            yield return package;
                        }
                    }
                }
            }

            yield break;
        }

        /// <summary>
        /// Retrieve a package with an id and version from a packages.config packages folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        /// <param name="identity">Package id and version.</param>
        public static LocalPackageInfo? GetPackagesConfigFolderPackage(string root, PackageIdentity identity, ILogger log)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            var rootDirInfo = GetAndVerifyRootDirectory(root);

            // Try matching the exact version format
            var idVersion = $"{identity.Id}.{identity.Version.ToString()}";
            var expectedPath = Path.Combine(rootDirInfo.FullName, idVersion, $"{idVersion}{PackagingCoreConstants.NupkgExtension}");
            var expectedFile = CreateFileInfoIfValidOrNull(expectedPath, log);

            if (expectedFile != null && expectedFile.Exists)
            {
                var localPackage = GetPackageFromNupkg(expectedFile);

                // Verify that the nuspec matches the expected id/version.
                if (localPackage != null && identity.Equals(localPackage.Identity))
                {
                    return localPackage;
                }
            }

            // Search all sub folders

            if (rootDirInfo.Exists)
            {
                var searchPattern = GetPackagesConfigFolderSearchPattern(identity.Id);

                foreach (var dir in GetDirectoriesSafe(rootDirInfo, searchPattern, SearchOption.TopDirectoryOnly, log, CancellationToken.None))
                {
                    // Check the id and version of the path, if the id matches and the version
                    // is valid this will be non-null;
                    var dirVersion = GetVersionFromIdVersionString(dir.Name, identity.Id);

                    if (identity.Version == dirVersion)
                    {
                        var localPackage = GetPackagesConfigFolderPackage(dir, log);

                        // Verify that the nuspec matches the expected id/version.
                        if (localPackage != null && identity.Equals(localPackage.Identity))
                        {
                            return localPackage;
                        }
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Returns either id.* or * depending on the OS.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private static string GetPackagesConfigFolderSearchPattern(string id)
        {
            // Case insensitive searches on windows may use the id prefix.
            // The majority of packages.config scenarios will be on windows.
            if (!string.IsNullOrEmpty(id) && RuntimeEnvironmentHelper.IsWindows)
            {
                return $"{id}.*";
            }

            // For non-windows systems which may be case-sensitive search all directories
            return $"*";
        }

        /// <summary>
        /// Retrieve a package with an id and version from a packages.config packages folder.
        /// </summary>
        /// <param name="root">Nupkg folder directory path.</param>
        public static LocalPackageInfo? GetPackagesConfigFolderPackage(
            string root,
            string id,
            NuGetVersion version,
            ILogger log)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

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

            var identity = new PackageIdentity(id, version);
            return GetPackagesConfigFolderPackage(root, identity, log);
        }

        /// <summary>
        /// Return the package nupkg from a packages.config folder sub directory.
        /// </summary>
        /// <param name="dir">Package directory in the format id.version</param>
        private static LocalPackageInfo? GetPackagesConfigFolderPackage(DirectoryInfo dir, ILogger log)
        {
            LocalPackageInfo? result = null;

            var nupkgPath = Path.Combine(
                dir.FullName,
                $"{dir.Name}{PackagingCoreConstants.NupkgExtension}");

            var nupkgFile = CreateFileInfoIfValidOrNull(nupkgPath, log);

            if (nupkgFile != null && nupkgFile.Exists)
            {
                result = GetPackageFromNupkg(nupkgFile);
            }

            return result;
        }

        /// <summary>
        /// True if the file name matches the identity. This is could be incorrect if
        /// the package name ends with numbers. The result should be checked against the nuspec.
        /// </summary>
        public static bool IsPossiblePackageMatch(FileInfo file, PackageIdentity identity)
        {
            if (identity == null)
            {
                throw new ArgumentNullException(nameof(identity));
            }

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

            return identity.Equals(GetIdentityFromNupkgPath(file, identity.Id));
        }

        /// <summary>
        /// True if the file name matches the id and is followed by a version. This is could be incorrect if
        /// the package name ends with numbers. The result should be checked against the nuspec.
        /// </summary>
        public static bool IsPossiblePackageMatch(FileInfo file, string id)
        {
            if (id == null)
            {
                throw new ArgumentNullException(nameof(id));
            }

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

            return GetIdentityFromNupkgPath(file, id) != null;
        }

        /// <summary>
        /// An imperfect attempt at finding the identity of a package from the file name.
        /// This can fail if the package name ends with something such as .1
        /// </summary>
        public static PackageIdentity? GetIdentityFromNupkgPath(FileInfo file, string id)
        {
            if (id == null)
            {
                throw new ArgumentNullException(nameof(id));
            }

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

            var version = GetVersionFromFileName(file.Name, id, PackagingCoreConstants.NupkgExtension);

            if (version != null)
            {
                return new PackageIdentity(id, version);
            }

            return null;
        }

        /// <summary>
        /// An imperfect attempt at finding the version of a package from the file name.
        /// This can fail if the package name ends with something such as .1
        /// </summary>
        public static NuGetVersion? GetVersionFromFileName(string fileName, string id, string extension)
        {
            if (id == null)
            {
                throw new ArgumentNullException(nameof(id));
            }

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

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

            NuGetVersion? result = null;

            if (fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
            {
                fileName = Path.GetFileNameWithoutExtension(fileName);

                // Skip symbol packages
                if (!fileName.EndsWith(".symbols", StringComparison.OrdinalIgnoreCase))
                {
                    result = GetVersionFromIdVersionString(fileName, id);
                }
            }

            return result;
        }

        /// <summary>
        /// Parse a possible version from a string in the format Id.Version
        /// Returns null if the version is invalid or the id did not match.
        /// </summary>
        /// <param name="idVersionString">Id.Version</param>
        /// <param name="id">Expected id</param>
        private static NuGetVersion? GetVersionFromIdVersionString(string idVersionString, string id)
        {
            var prefix = $"{id}.";

            if (idVersionString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                var versionString = idVersionString.Substring(prefix.Length);

                NuGetVersion? version;
                if (NuGetVersion.TryParse(versionString, out version))
                {
                    return version;
                }
            }

            return null;
        }

        /// <summary>
        /// Retrieve a single package from a v3 version folder.
        /// </summary>
        public static LocalPackageInfo? GetPackageV3(string root, string id, NuGetVersion version, ILogger log)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

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

            return GetPackageV3(root, new PackageIdentity(id, version), log);
        }

        /// <summary>
        /// Retrieve a package from a v3 feed.
        /// </summary>
        public static LocalPackageInfo? GetPackageV3(string root, PackageIdentity identity, ILogger log)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            // Verify the root path is a valid path.
            var rootDirInfo = GetAndVerifyRootDirectory(root);

            var pathResolver = new VersionFolderPathResolver(rootDirInfo.FullName);

            // Verify the neccessary files exist
            var nupkgPath = pathResolver.GetPackageFilePath(identity.Id, identity.Version);
            var nuspecPath = pathResolver.GetManifestFilePath(identity.Id, identity.Version);
            var hashPath = pathResolver.GetHashPath(identity.Id, identity.Version);

            if (!File.Exists(nupkgPath))
            {
                log.LogDebug($"Missing {nupkgPath}");
                return null;
            }

            if (!File.Exists(nuspecPath))
            {
                log.LogDebug($"Missing {nuspecPath}");
                return null;
            }

            if (!File.Exists(hashPath))
            {
                log.LogDebug($"Missing {hashPath}");
                return null;
            }

            var nuspecHelper = new Lazy<NuspecReader>(() => new NuspecReader(nuspecPath));

            return new LocalPackageInfo(
                new PackageIdentity(identity.Id, identity.Version),
                nupkgPath,
                File.GetLastWriteTimeUtc(nupkgPath),
                nuspecHelper,
                useFolder: false
            );
        }

        /// <summary>
        /// Discover all nupkgs from a v2 local folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        public static IEnumerable<FileInfo> GetNupkgsFromFlatFolder(string root, ILogger log)
        {
            return GetNupkgsFromFlatFolder(root, log, CancellationToken.None);
        }

        /// <summary>
        /// Discover all nupkgs from a v2 local folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<FileInfo> GetNupkgsFromFlatFolder(string root, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

            // Check for package files one level deep.
            DirectoryInfo rootDirectoryInfo = GetAndVerifyRootDirectory(root);

            // Return all directory file list chunks in a flat list
            foreach (var directoryList in GetNupkgsFromFlatFolderChunked(rootDirectoryInfo, log, cancellationToken))
            {
                foreach (var file in directoryList)
                {
                    yield return file;
                }
            }

            yield break;
        }

        public static FeedType GetLocalFeedType(string root, ILogger log)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

            // Check for package files one level deep.
            DirectoryInfo rootDirectoryInfo = GetAndVerifyRootDirectory(root);

            try
            {
                // Search root directory for *.Nupkg, return V2 if there is any Nupkg file.
                if (rootDirectoryInfo.EnumerateFiles(NupkgFilter, SearchOption.TopDirectoryOnly).Any())
                {
                    return FeedType.FileSystemV2;
                }

                foreach (var idDir in rootDirectoryInfo.EnumerateDirectories())
                {
                    // Search first sub directory for *.Nupkg, return V2 if there is any Nupkg file.
                    if (idDir.EnumerateFiles(NupkgFilter, SearchOption.TopDirectoryOnly).Any())
                    {
                        return FeedType.FileSystemV2;
                    }

                    foreach (var versionDir in idDir.EnumerateDirectories())
                    {
                        // If we have files in the format {packageId}/{version}/{packageId}.{version}.nupkg, return V3. 
                        var package = GetPackageV3(root, idDir.Name, versionDir.Name, log);

                        if (package != null)
                        {
                            return FeedType.FileSystemV3;
                        }
                    }
                }
            }

            catch (UnauthorizedAccessException)
            {

            }
            catch (DirectoryNotFoundException)
            {

            }

            return FeedType.FileSystemUnknown;
        }

        /// <summary>
        /// Verify that a path could be a valid directory. Throw a FatalProtocolException otherwise.
        /// </summary>
        public static DirectoryInfo GetAndVerifyRootDirectory(string root)
        {
            try
            {
                // Convert file:// to a local path if needed
                var localPath = UriUtility.GetLocalPath(root);

                // Verify that the directory is a valid path.
                var rootDirectoryInfo = new DirectoryInfo(localPath);

                // The root must also be parsable as a URI (relative or absolute). This rejects
                // sources that have the weird "C:Source" format. For more information about this 
                // format, see:
                // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#paths
                var uriResult = new Uri(root, UriKind.RelativeOrAbsolute);

                // Allow only local paths
                if (uriResult?.IsAbsoluteUri == true && !uriResult.IsFile)
                {
                    throw new NotSupportedException(uriResult.AbsoluteUri);
                }

                return rootDirectoryInfo;
            }
            catch (Exception ex) when (ex is ArgumentException ||
                                       ex is IOException ||
                                       ex is SecurityException ||
                                       ex is UriFormatException ||
                                       ex is NotSupportedException)
            {
                var message = string.Format(CultureInfo.CurrentCulture, Strings.Log_FailedToVerifyRootDirectory, root);

                throw new FatalProtocolException(message, ex);
            }
        }

        /// <summary>
        /// Verify that a path could be a valid file. Throw a FatalProtocolException otherwise.
        /// </summary>
        private static FileInfo GetAndVerifyFileInfo(Uri fileUri)
        {
            try
            {
                // Verify that the file is a valid path
                return new FileInfo(fileUri.LocalPath);
            }
            catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is SecurityException)
            {
                var message = string.Format(CultureInfo.CurrentCulture, Strings.Log_FailedToVerifyValidFile, fileUri.AbsoluteUri);

                throw new FatalProtocolException(message, ex);
            }
        }

        /// <summary>
        /// Retrieve files in chunks, this helps maintain the legacy behavior of searching for
        /// certain non-normalized file names.
        /// </summary>
        private static IEnumerable<List<FileInfo>> GetNupkgsFromFlatFolderChunked(DirectoryInfo root, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

            // Ignore missing directories for v2
            if (!root.Exists)
            {
                yield break;
            }

            cancellationToken.ThrowIfCancellationRequested();

            // Search the top level directory
            var topLevel = GetNupkgsFromDirectory(root, log, cancellationToken);

            if (topLevel.Count > 0)
            {
                yield return topLevel;
            }

            // Search all sub directories
            foreach (var subDirectory in GetDirectoriesSafe(root, log, cancellationToken))
            {
                var files = GetNupkgsFromDirectory(subDirectory, log, cancellationToken);

                if (files.Count > 0)
                {
                    yield return files;
                }
            }

            yield break;
        }

        /// <summary>
        /// Discover nupkgs from a v2 local folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        /// <param name="id">Package id file name prefix.</param>
        public static IEnumerable<FileInfo> GetNupkgsFromFlatFolder(string root, string id, ILogger log)
        {
            return GetNupkgsFromFlatFolder(root, id, log, CancellationToken.None);
        }

        /// <summary>
        /// Discover nupkgs from a v2 local folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<FileInfo> GetNupkgsFromFlatFolder(string root, string id, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            foreach (var path in GetNupkgsFromFlatFolder(root, log, cancellationToken))
            {
                if (IsPossiblePackageMatch(path, id))
                {
                    yield return path;
                }
            }

            yield break;
        }

        /// <summary>
        /// Discover all nupkgs from a v3 folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV3(string root, ILogger log)
        {
            return GetPackagesV3(root, log, CancellationToken.None);
        }

        /// <summary>
        /// Discover all nupkgs from a v3 folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV3(string root, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

            cancellationToken.ThrowIfCancellationRequested();
            // Validate teh root path
            DirectoryInfo rootDirectoryInfo = GetAndVerifyRootDirectory(root);

            if (!rootDirectoryInfo.Exists)
            {
                // Directory is missing
                yield break;
            }

            // Match all nupkgs in the folder
            foreach (var idPath in GetDirectoriesSafe(rootDirectoryInfo, log, cancellationToken))
            {
                foreach (var nupkg in GetPackagesV3(root, id: idPath.Name, log: log, cancellationToken: cancellationToken))
                {
                    yield return nupkg;
                }
            }

            yield break;
        }

        /// <summary>
        /// Discover nupkgs from a v3 local folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        /// <param name="id">Package id or package id prefix.</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV3(string root, string id, ILogger log)
        {
            return GetPackagesV3(root, id, log, CancellationToken.None);
        }

        /// <summary>
        /// Discover nupkgs from a v3 local folder.
        /// </summary>
        /// <param name="root">Folder root.</param>
        /// <param name="id">Package id or package id prefix.</param>
        /// <param name="cancellationToken">Cancellation token</param>
        public static IEnumerable<LocalPackageInfo> GetPackagesV3(string root, string id, ILogger log, CancellationToken cancellationToken)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

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

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

            cancellationToken.ThrowIfCancellationRequested();
            // Check for package files one level deep.
            DirectoryInfo rootDirectoryInfo = GetAndVerifyRootDirectory(root);

            var pathResolver = new VersionFolderPathResolver(rootDirectoryInfo.FullName);
            var idRoot = new DirectoryInfo(pathResolver.GetVersionListPath(id));
            if (!idRoot.Exists)
            {
                // Directory is missing
                yield break;
            }

            foreach (var versionDir in GetDirectoriesSafe(idRoot, log, cancellationToken))
            {
                var package = GetPackageV3(root, id, versionDir.Name, log);

                if (package != null)
                {
                    yield return package;
                }
            }

            yield break;
        }

        /// <summary>
        /// Resolves a package path into a list of paths.
        /// If the path contains wildcards then the path is expanded to all matching entries.
        /// </summary>
        /// <param name="packagePath">Package path</param>
        /// <returns>A list of package paths that match the input path.</returns>
        public static IEnumerable<string> ResolvePackageFromPath(string packagePath, bool isSnupkg = false)
        {
            packagePath = EnsurePackageExtension(packagePath, isSnupkg);
            return PathResolver.PerformWildcardSearch(Directory.GetCurrentDirectory(), packagePath);
        }

        /// <summary>
        /// Ensure any wildcards in packagePath end with *.nupkg or *.snupkg.
        /// </summary>
        /// <param name="packagePath"></param>
        /// <param name="isSnupkg"></param>
        /// <returns>The absolute path, or the normalized wildcard path.</returns>
        private static string EnsurePackageExtension(string packagePath, bool isSnupkg)
        {
#if NETCOREAPP
            if (packagePath.IndexOf('*', StringComparison.Ordinal) == -1)
#else
            if (packagePath.IndexOf('*') == -1)
#endif
            {
                // If there's no wildcard in the path to begin with, assume that it's an absolute path.
                return packagePath;
            }
            // If the path does not contain wildcards, we need to add *.nupkg to it.
            if (!packagePath.EndsWith(NuGetConstants.PackageExtension, StringComparison.OrdinalIgnoreCase)
                && !packagePath.EndsWith(NuGetConstants.SnupkgExtension, StringComparison.OrdinalIgnoreCase))
            {
                if (packagePath.EndsWith("**", StringComparison.OrdinalIgnoreCase))
                {
                    packagePath = packagePath + Path.DirectorySeparatorChar + '*';
                }
                else if (!packagePath.EndsWith("*", StringComparison.OrdinalIgnoreCase))
                {
                    packagePath = packagePath + '*';
                }
                packagePath = packagePath + (isSnupkg ? NuGetConstants.SnupkgExtension : NuGetConstants.PackageExtension);
            }
            return packagePath;
        }

        /// <summary>
        /// If there isn't at least one Path specified, throw that no file paths were resolved for this Package.
        /// </summary>
        /// <param name="packagePath">The package path the user originally provided.</param>
        /// <param name="matchingPackagePaths">A list of matching package paths that were previously resolved.</param>
        public static void EnsurePackageFileExists(string packagePath, IEnumerable<string> matchingPackagePaths)
        {
            if (!(matchingPackagePaths != null && matchingPackagePaths.Any()))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    Strings.UnableToFindFile,
                    packagePath));
            }
        }

        /// <summary>
        /// Remove duplicate packages which can occur in directories.
        /// In V2 packages may exist under multiple sub folders. 
        /// Non-normalized versions also lead to duplicates: ex: 1.0, 1.0.0.0
        /// </summary>
        public static IEnumerable<LocalPackageInfo> GetDistinctPackages(IEnumerable<LocalPackageInfo> packages)
        {
            if (packages == null)
            {
                throw new ArgumentNullException(nameof(packages));
            }

            var seen = new HashSet<PackageIdentity>();

            foreach (var package in packages)
            {
                if (seen.Add(package.Identity))
                {
                    yield return package;
                }
            }

            yield break;
        }

        /// <summary>
        /// Retrieve directories and log exceptions that occur.
        /// </summary>
        private static List<DirectoryInfo> GetDirectoriesSafe(DirectoryInfo root, ILogger log, CancellationToken cancellationToken)
        {
            try
            {
                var enumerable = root.EnumerateDirectories();
                // .ToList necessary for perf concern.
                // If enumaration happen several times then same I/O calls repeatedly called on same input, I/O calls are more expensive then memory.
                return CancellableYieldEnumeration(enumerable, cancellationToken).ToList();
            }
            catch (Exception e) when (e is not OperationCanceledException)
            {
                // On cancellation we bubble up exception to call stack.
                // Otherwise return all or nothing. If no exception return all.
                // Break on first exception with logging in order to keep previous experience, return empty List, but don't throw.
                log.LogWarning(e.Message);
            }

            return new List<DirectoryInfo>();
        }

        private static List<DirectoryInfo> GetDirectoriesSafe(DirectoryInfo root, string filter, SearchOption searchOption, ILogger log, CancellationToken cancellationToken)
        {
            try
            {
                var enumerable = root.EnumerateDirectories(filter, searchOption);
                // .ToList necessary for perf concern.
                // If enumaration happen several times then same I/O calls repeatedly called on same input, I/O calls are more expensive then memory.
                return CancellableYieldEnumeration(enumerable, cancellationToken).ToList();
            }
            catch (Exception e) when (e is not OperationCanceledException)
            {
                // On cancellation we bubble up exception to call stack.
                // Otherwise return all or nothing. If no exception return all.
                // Break on first exception with logging in order to keep previous experience, return empty List, but don't throw.
                log.LogWarning(e.Message);
            }

            return new List<DirectoryInfo>();
        }

        /// <summary>
        /// Retrieve files and log exceptions that occur.
        /// </summary>
        internal static List<FileInfo> GetFilesSafe(DirectoryInfo root, string filter, ILogger log, CancellationToken cancellationToken)
        {
            try
            {
                var enumerable = root.EnumerateFiles(filter);
                // .ToList necessary for perf concern.
                // If enumaration happen several times then same I/O calls repeatedly called on same input, I/O calls are more expensive then memory.
                return CancellableYieldEnumeration(enumerable, cancellationToken).ToList();
            }
            catch (Exception e) when (e is not OperationCanceledException)
            {
                // On cancellation we bubble up exception to call stack.
                // Otherwise return all or nothing. If no exception return all.
                // Break on first exception with logging in order to keep previous experience, return empty List, but don't throw.
                log.LogWarning(e.Message);
            }

            return new List<FileInfo>();
        }

        /// <summary>
        /// Path -> LocalPackageInfo
        /// </summary>
        private static IEnumerable<LocalPackageInfo> GetPackagesFromNupkgs(IEnumerable<FileInfo> files, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return files.Select(file => GetPackageFromNupkg(file));
        }

        /// <summary>
        /// Path -> LocalPackageInfo
        /// </summary>
        private static LocalPackageInfo GetPackageFromNupkg(FileInfo nupkgFile)
        {
            try
            {
                using (var package = new PackageArchiveReader(nupkgFile.FullName))
                {
                    var nuspec = package.NuspecReader;

                    var nuspecHelper = new Lazy<NuspecReader>(() => nuspec);

                    return new LocalPackageInfo(
                        nuspec.GetIdentity(),
                        nupkgFile.FullName,
                        nupkgFile.LastWriteTimeUtc,
                        nuspecHelper,
                        useFolder: false
                    );
                }
            }
            catch (Exception ex)
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.NupkgPath_InvalidEx,
                    nupkgFile.FullName,
                    ex.Message);

                throw new FatalProtocolException(message, ex);
            }
        }

        /// <summary>
        /// Find all nupkgs in the top level of a directory.
        /// </summary>
        private static List<FileInfo> GetNupkgsFromDirectory(DirectoryInfo root, ILogger log, CancellationToken cancellationToken)
        {
            return GetFilesSafe(root, NupkgFilter, log, cancellationToken);
        }

        private static LocalPackageInfo? GetPackageV3(string root, string id, string version, ILogger log)
        {
            NuGetVersion? nugetVersion;
            if (NuGetVersion.TryParse(version, out nugetVersion))
            {
                var identity = new PackageIdentity(id, nugetVersion);

                // Read the package, this may be null if files are missing
                return GetPackageV3(root, identity, log);
            }
            else
            {
                log.LogWarning(string.Format(CultureInfo.CurrentCulture, Strings.UnableToParseFolderV3Version, version));
            }

            return null;
        }

        private static FileInfo? CreateFileInfoIfValidOrNull(string localPath, ILogger log)
        {
            FileInfo? fileInfo = null;
            try
            {
                fileInfo = new FileInfo(localPath);
            }
            catch (PathTooLongException e)
            {
                log.LogDebug(e.Message);
            }
            return fileInfo;
        }

        public static void GenerateNupkgMetadataFile(string nupkgPath, string installPath, string hashPath, string nupkgMetadataPath)
        {
            ConcurrencyUtilities.ExecuteWithFileLocked(nupkgPath,
                action: () =>
                {
                    // make sure new hash file doesn't exists within File lock before actually creating it.
                    if (!File.Exists(nupkgMetadataPath))
                    {
                        var tempNupkgMetadataFilePath = Path.Combine(installPath, Path.GetRandomFileName());
                        using (var stream = File.Open(nupkgPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                        using (var packageReader = new PackageArchiveReader(stream))
                        {
                            // get hash of unsigned content of signed package
                            var packageHash = packageReader.GetContentHash(
                                CancellationToken.None,
                                GetUnsignedPackageHash:
                                () =>
                                {
                                    if (!string.IsNullOrEmpty(hashPath) && File.Exists(hashPath))
                                    {
                                        return File.ReadAllText(hashPath);
                                    }

                                    // Callers guarantee hashPath exists, so this branch is unreachable.
                                    // null! is required because GetContentHash's delegate is typed as Func<string> (not Func<string?>).
                                    return null!;
                                });

                            // write the new hash file
                            var hashFile = new NupkgMetadataFile()
                            {
                                ContentHash = packageHash
                            };

                            NupkgMetadataFileFormat.Write(tempNupkgMetadataFilePath, hashFile);
                            File.Move(tempNupkgMetadataFilePath, nupkgMetadataPath);
                        }
                    }
                });
        }

        static IEnumerable<T> CancellableYieldEnumeration<T>(IEnumerable<T> enumerable, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            foreach (T item in enumerable)
            {
                cancellationToken.ThrowIfCancellationRequested();
                yield return item;
            }
        }
    }
}