File: PackageExtraction\PackagePathHelper.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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using NuGet.Common;
using NuGet.Packaging.Core;
using NuGet.Versioning;

namespace NuGet.Packaging
{
    // HACK: TODO: This entire class is a hack. This is in place only for back-compat reasons
    // If the version was always normalized for package directory and package file name, there would be no issue :(
    public static class PackagePathHelper
    {
        internal static IEnumerable<string> GetFiles(string root, string path, string filter, bool recursive)
        {
            path = PathUtility.EnsureTrailingSlash(Path.Combine(root, path));
            if (string.IsNullOrEmpty(filter))
            {
                filter = "*.*";
            }
            try
            {
                if (!Directory.Exists(path))
                {
                    return Enumerable.Empty<string>();
                }
                return Directory.EnumerateFiles(path, filter, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
            }
            catch (UnauthorizedAccessException)
            {
            }
            catch (DirectoryNotFoundException)
            {
            }

            return Enumerable.Empty<string>();
        }

        internal static IEnumerable<string> GetDirectories(string root, string path)
        {
            try
            {
                path = PathUtility.EnsureTrailingSlash(Path.Combine(root, path));
                if (!Directory.Exists(path))
                {
                    return Enumerable.Empty<string>();
                }
                return Directory.EnumerateDirectories(path);
            }
            catch (UnauthorizedAccessException)
            {
            }
            catch (DirectoryNotFoundException)
            {
            }

            return Enumerable.Empty<string>();
        }

        private static IEnumerable<string> GetPackageFiles(string root, string filter)
        {
            filter = filter ?? "*" + PackagingCoreConstants.NupkgExtension;
            Debug.Assert(
                filter.EndsWith(PackagingCoreConstants.NupkgExtension, StringComparison.OrdinalIgnoreCase) ||
                filter.EndsWith(PackagingCoreConstants.NuspecExtension, StringComparison.OrdinalIgnoreCase));

            // Check for package files one level deep. We use this at package install time
            // to determine the set of installed packages. Installed packages are copied to 
            // {id}.{version}\{packagefile}.{extension}.
            foreach (var dir in GetDirectories(root, string.Empty))
            {
                foreach (var path in GetFiles(root, dir, filter, recursive: false))
                {
                    yield return path;
                }
            }

            // Check top level directory
            foreach (var path in GetFiles(root, string.Empty, filter, recursive: false))
            {
                yield return path;
            }
        }

        private static bool FileNameMatchesPattern(PackageIdentity packageIdentity, string path)
        {
            var packageId = packageIdentity.Id;
            var name = Path.GetFileNameWithoutExtension(path);
            NuGetVersion? parsedVersion;

            // When matching by pattern, we will always have a version token. Packages without versions would be matched early on by the version-less path resolver 
            // when doing an exact match.
            return name.Length > packageId.Length &&
                   NuGetVersion.TryParse(name.Substring(packageId.Length + 1), out parsedVersion) &&
                   parsedVersion.Equals(packageIdentity.Version);
        }

        public static IEnumerable<string> GetPackageLookupPaths(PackageIdentity packageIdentity, PackagePathResolver packagePathResolver)
        {
            if (packageIdentity == null)
            {
                throw new ArgumentNullException(nameof(packageIdentity));
            }

            if (packageIdentity.Version == null)
            {
                throw new ArgumentException(
                    string.Format(CultureInfo.CurrentCulture, Strings.PropertyCannotBeNull, nameof(packageIdentity.Version)),
                    nameof(packageIdentity));
            }

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

            var packageId = packageIdentity.Id.ToLowerInvariant();
            var version = packageIdentity.Version;

            var root = packagePathResolver.Root;
            // Files created by the path resolver. This would take into account the non-side-by-side scenario 
            // and we do not need to match this for id and version.
            var packageFileName = packagePathResolver.GetPackageFileName(packageIdentity);
            var manifestFileName = Path.ChangeExtension(packageFileName, PackagingCoreConstants.NuspecExtension);
            var filesMatchingFullName = Enumerable.Concat(
                GetPackageFiles(root, packageFileName),
                GetPackageFiles(root, manifestFileName));

            if (version != null
                && version.Version.Revision < 1)
            {
                // If the build or revision number is not set, we need to look for combinations of the format
                // * Foo.1.2.nupkg
                // * Foo.1.2.3.nupkg
                // * Foo.1.2.0.nupkg
                // * Foo.1.2.0.0.nupkg
                // To achieve this, we would look for files named 1.2*.nupkg if both build and revision are 0 and
                // 1.2.3*.nupkg if only the revision is set to 0.
                var partialName = version.Version.Build < 1 ?
                    string.Join(".", packageId, version.Version.Major, version.Version.Minor) :
                    string.Join(".", packageId, version.Version.Major, version.Version.Minor, version.Version.Build);
                var partialManifestName = partialName + "*" + PackagingCoreConstants.NuspecExtension;
                partialName += "*" + PackagingCoreConstants.NupkgExtension;

                // Partial names would result is gathering package with matching major and minor but different build and revision. 
                // Attempt to match the version in the path to the version we're interested in.
                var partialNameMatches = GetPackageFiles(root, partialName).Where(path => FileNameMatchesPattern(packageIdentity, path));
                var partialManifestNameMatches = GetPackageFiles(root, partialManifestName).Where(
                    path => FileNameMatchesPattern(packageIdentity, path));
                return Enumerable.Concat(filesMatchingFullName, partialNameMatches).Concat(partialManifestNameMatches);
            }
            return filesMatchingFullName;
        }

        public static string? GetInstalledPackageFilePath(PackageIdentity packageIdentity, PackagePathResolver packagePathResolver)
        {
            var packageLookupPaths = GetPackageLookupPaths(packageIdentity, packagePathResolver);
            // TODO: Not handling nuspec-only scenarios
            foreach (var packageLookupPath in packageLookupPaths)
            {
                if (packageLookupPath.EndsWith(PackagingCoreConstants.NupkgExtension, StringComparison.OrdinalIgnoreCase) &&
                    File.Exists(packageLookupPath))
                {
                    // This is an installed package lookup path which matches the packageIdentity for the given packagePathResolver
                    return packageLookupPath;
                }
            }

            return null;
        }
    }
}