// 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. #nullable disable using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Evaluation; using NuGet.CommandLine.XPlat.ListPackage; using NuGet.CommandLine.XPlat.Utility; using NuGet.Common; using NuGet.Configuration; using NuGet.ProjectModel; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Protocol.Model; using NuGet.Protocol.Providers; using NuGet.Protocol.Resources; using NuGet.Versioning; namespace NuGet.CommandLine.XPlat { internal class ListPackageCommandRunner : IListPackageCommandRunner { private const string ProjectAssetsFile = "ProjectAssetsFile"; private const string ProjectName = "MSBuildProjectName"; private const int GenericSuccessExitCode = 0; private const int GenericFailureExitCode = 1; private readonly MSBuildAPIUtility _msbuildUtility; private readonly Dictionary<PackageSource, SourceRepository> _sourceRepositoryCache; public ListPackageCommandRunner(MSBuildAPIUtility msbuildUtility) { _msbuildUtility = msbuildUtility; _sourceRepositoryCache = new Dictionary<PackageSource, SourceRepository>(); } public async Task<int> ExecuteCommandAsync(ListPackageArgs listPackageArgs) { IReportRenderer reportRenderer = listPackageArgs.Renderer; (int exitCode, ListPackageReportModel reportModel) = await GetReportDataAsync(listPackageArgs); reportRenderer.Render(reportModel); return exitCode; } internal async Task<(int, ListPackageReportModel)> GetReportDataAsync(ListPackageArgs listPackageArgs) { // It's important not to print anything to console from below methods and sub method calls, because it'll affect both json/console outputs. var listPackageReportModel = new ListPackageReportModel(listPackageArgs); if (!File.Exists(listPackageArgs.Path)) { listPackageArgs.Renderer.AddProblem(problemType: ProblemType.Error, text: string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorFileNotFound, listPackageArgs.Path)); return (GenericFailureExitCode, listPackageReportModel); } PopulateSourceRepositoryCache(listPackageArgs); //If the given file is a solution, get the list of projects //If not, then it's a project, which is put in a list string fileExtension = Path.GetExtension(listPackageArgs.Path); var projectsPaths = (fileExtension.Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) || fileExtension.Equals(".slnx", PathUtility.GetStringComparisonBasedOnOS()) || fileExtension.Equals(".slnf", PathUtility.GetStringComparisonBasedOnOS())) ? MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path).Where(File.Exists) : [listPackageArgs.Path]; foreach (string projectPath in projectsPaths) { await GetProjectMetadataAsync(projectPath, listPackageReportModel, listPackageArgs); } // if there is any error then return failure code. int exitCode = ( listPackageArgs.Renderer.GetProblems().Any(p => p.ProblemType == ProblemType.Error) || listPackageReportModel.Projects.Where(p => p.ProjectProblems != null).SelectMany(p => p.ProjectProblems).Any(p => p.ProblemType == ProblemType.Error)) ? GenericFailureExitCode : GenericSuccessExitCode; return (exitCode, listPackageReportModel); } private async Task GetProjectMetadataAsync( string projectPath, ListPackageReportModel listPackageReportModel, ListPackageArgs listPackageArgs) { //Open project to evaluate properties for the assets //file and the name of the project Project project = _msbuildUtility.GetProject(projectPath).Project; var projectName = project.GetPropertyValue(ProjectName); ListPackageProjectModel projectModel = listPackageReportModel.CreateProjectReportData(projectPath: projectPath, projectName); if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) { projectModel.AddProjectInformation(problemType: ProblemType.Error, string.Format(CultureInfo.CurrentCulture, Strings.Error_NotPRProject, projectPath)); return; } var assetsPath = project.GetPropertyValue(ProjectAssetsFile); if (!IsProjectAssetsFileValid(assetsPath, projectPath, projectModel, out LockFile assetsFile)) { return; } foreach (string frameworkAlias in listPackageArgs.Frameworks) { if (assetsFile.PackageSpec?.GetTargetFramework(frameworkAlias) == null) { projectModel.AddProjectInformation(problemType: ProblemType.Error, string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_InvalidFramework, frameworkAlias, projectPath)); return; } } List<FrameworkPackages> frameworks; try { frameworks = MSBuildAPIUtility.GetResolvedVersions(project, listPackageArgs.Frameworks, assetsFile, listPackageArgs.IncludeTransitive); } catch (InvalidOperationException ex) { projectModel.AddProjectInformation(ProblemType.Error, ex.Message); return; } if (frameworks.Count > 0) { bool vulnerabilitiesCheckedFromAuditSources = false; if (listPackageArgs.ReportType != ReportType.Default) // generic list package is offline -- no server lookups { List<PackageSource> httpSources = HttpSourcesUtility.GetDisallowedInsecureHttpSources(listPackageArgs.PackageSources); httpSources.AddRange(HttpSourcesUtility.GetDisallowedInsecureHttpSources(listPackageArgs.AuditSources)); if (httpSources.Count > 0) { projectModel.AddProjectInformation(ProblemType.Error, HttpSourcesUtility.BuildHttpSourceErrorMessage(httpSources, "list package")); return; } if (listPackageArgs.ReportType == ReportType.Vulnerable && listPackageArgs.AuditSources != null && listPackageArgs.AuditSources.Count > 0) { await GetVulnerabilitiesFromAuditSourcesAsync(listPackageArgs, listPackageReportModel, projectModel, frameworks); vulnerabilitiesCheckedFromAuditSources = true; } else { var metadata = await GetPackageMetadataAsync(frameworks, listPackageArgs); await UpdatePackagesWithSourceMetadata(frameworks, metadata, listPackageArgs); } } if (!vulnerabilitiesCheckedFromAuditSources) { bool filterPackages = FilterPackages(frameworks, listPackageArgs) || ReportType.Default == listPackageArgs.ReportType; if (filterPackages) { var hasAutoReference = false; List<ListPackageReportFrameworkPackage> projectFrameworkPackages = ProjectPackagesPrintUtility.GetPackagesMetadata(frameworks, listPackageArgs, ref hasAutoReference); projectModel.TargetFrameworkPackages = projectFrameworkPackages; projectModel.AutoReferenceFound = hasAutoReference; } else { projectModel.TargetFrameworkPackages = new List<ListPackageReportFrameworkPackage>(); } } } } private static async Task GetVulnerabilitiesFromAuditSourcesAsync( ListPackageArgs listPackageArgs, ListPackageReportModel listPackageReportModel, ListPackageProjectModel projectModel, List<FrameworkPackages> frameworks) { List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilities = await GetVulnerabilityData( projectModel, listPackageReportModel, listPackageArgs.AuditSources, listPackageArgs.Logger, listPackageArgs.CancellationToken); foreach (var frameworkPackages in frameworks) { var frameworkPackage = new ListPackageReportFrameworkPackage(frameworkPackages.Framework, frameworkPackages.TargetAlias) { TransitivePackages = new List<ListReportPackage>(), TopLevelPackages = new List<ListReportPackage>() }; ProcessPackages(frameworkPackages.TopLevelPackages, vulnerabilities, frameworkPackage.TopLevelPackages); ProcessPackages(frameworkPackages.TransitivePackages, vulnerabilities, frameworkPackage.TransitivePackages); projectModel.TargetFrameworkPackages ??= new List<ListPackageReportFrameworkPackage>(); projectModel.TargetFrameworkPackages.Add(frameworkPackage); } } private static void ProcessPackages( IEnumerable<InstalledPackageReference> packages, List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilities, List<ListReportPackage> reportPackages) { foreach (var package in packages) { var vuln = GetPackageVulnerabilities( vulnerabilities, package.Name, package.ResolvedPackageMetadata.Identity.Version.ToNormalizedString() ).ToList(); if (vuln != null && vuln.Count > 0) { reportPackages.Add( new ListReportPackage( package.Name, package.OriginalRequestedVersion, package.ResolvedPackageMetadata.Identity.Version.ToString(), vuln)); } } } private static bool IsProjectAssetsFileValid(string assetsPath, string projectPath, ListPackageProjectModel projectModel, out LockFile assetsFile) { assetsFile = null; if (!File.Exists(assetsPath)) { projectModel.AddProjectInformation(ProblemType.Error, string.Format(CultureInfo.CurrentCulture, Strings.Error_AssetsFileNotFound, projectPath)); return false; } else { var lockFileFormat = new LockFileFormat(); assetsFile = lockFileFormat.Read(assetsPath); // Assets file validation if (assetsFile.PackageSpec == null || assetsFile.Targets == null || assetsFile.Targets.Count == 0) { projectModel.AddProjectInformation(ProblemType.Error, string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsPath)); return false; } else { return true; } } } private static async Task<List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>>> GetVulnerabilityData( ListPackageProjectModel projectModel, ListPackageReportModel reportModel, IReadOnlyList<PackageSource> sources, ILogger logger, CancellationToken cancellationToken) { var vulnerabilityInfo = new List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>>(); foreach (var source in sources) { if (!await TryAddSourceVulnerabilityInfo(source, reportModel, logger, cancellationToken, vulnerabilityInfo)) { projectModel.AddProjectInformation( ProblemType.Warning, string.Format(CultureInfo.CurrentCulture, Strings.Warning_AuditSourceWithoutData, source.Name) ); } } return vulnerabilityInfo; } private static async Task<bool> TryAddSourceVulnerabilityInfo( PackageSource source, ListPackageReportModel reportModel, ILogger logger, CancellationToken cancellationToken, List<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilityInfo) { var repository = Repository.Factory.GetCoreV3(source); var vulnerabilityProvider = new VulnerabilityInfoResourceV3Provider(); var (isCreated, resource) = await vulnerabilityProvider.TryCreate(repository, cancellationToken); if (!isCreated || resource is not VulnerabilityInfoResourceV3 vulnerabilityResource) { return false; } reportModel.AuditSourcesUsed.Add(source); var vulnerabilityInfoResult = await vulnerabilityResource.GetVulnerabilityInfoAsync( new SourceCacheContext(), logger, cancellationToken ); if (vulnerabilityInfoResult?.KnownVulnerabilities != null) { vulnerabilityInfo.AddRange(vulnerabilityInfoResult.KnownVulnerabilities); } return true; } private static IEnumerable<PackageVulnerabilityMetadata> GetPackageVulnerabilities( IEnumerable<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> vulnerabilities, string id, string version) { if (vulnerabilities == null) { return Enumerable.Empty<PackageVulnerabilityMetadata>(); } var parsedVersion = new NuGetVersion(version); foreach (var vulnFile in vulnerabilities) { if (vulnFile.TryGetValue(id, out IReadOnlyList<PackageVulnerabilityInfo> vulnPackages) && vulnPackages != null) { return vulnPackages .Where(package => package.Versions.Satisfies(parsedVersion)) .Select(v => new PackageVulnerabilityMetadata(v.Url, (int)v.Severity)) .ToList(); } } return Enumerable.Empty<PackageVulnerabilityMetadata>(); } public static bool FilterPackages(IEnumerable<FrameworkPackages> packages, ListPackageArgs listPackageArgs) { switch (listPackageArgs.ReportType) { case ReportType.Default: break; // No filtering in this case case ReportType.Outdated: FilterPackages( packages, ListPackageHelper.TopLevelPackagesFilterForOutdated, ListPackageHelper.TransitivePackagesFilterForOutdated); break; case ReportType.Deprecated: FilterPackages( packages, ListPackageHelper.PackagesFilterForDeprecated, ListPackageHelper.PackagesFilterForDeprecated); break; case ReportType.Vulnerable: FilterPackages( packages, ListPackageHelper.PackagesFilterForVulnerable, ListPackageHelper.PackagesFilterForVulnerable); break; } return packages.Any(p => p.TopLevelPackages.Any() || listPackageArgs.IncludeTransitive && p.TransitivePackages.Any()); } /// <summary> /// Filters top-level and transitive packages. /// </summary> /// <param name="packages">The <see cref="FrameworkPackages"/> to filter.</param> /// <param name="topLevelPackagesFilter">The filter to be applied on all <see cref="FrameworkPackages.TopLevelPackages"/>.</param> /// <param name="transitivePackagesFilter">The filter to be applied on all <see cref="FrameworkPackages.TransitivePackages"/>.</param> private static void FilterPackages( IEnumerable<FrameworkPackages> packages, Func<InstalledPackageReference, bool> topLevelPackagesFilter, Func<InstalledPackageReference, bool> transitivePackagesFilter) { foreach (var frameworkPackages in packages) { frameworkPackages.TopLevelPackages = GetInstalledPackageReferencesWithFilter( frameworkPackages.TopLevelPackages, topLevelPackagesFilter); frameworkPackages.TransitivePackages = GetInstalledPackageReferencesWithFilter( frameworkPackages.TransitivePackages, transitivePackagesFilter); } } private static IEnumerable<InstalledPackageReference> GetInstalledPackageReferencesWithFilter( IEnumerable<InstalledPackageReference> references, Func<InstalledPackageReference, bool> filter) { var filteredReferences = new List<InstalledPackageReference>(); foreach (var reference in references) { if (filter(reference)) { filteredReferences.Add(reference); } } return filteredReferences; } /// <summary> /// Get package metadata from all sources /// </summary> /// <param name="targetFrameworks">A <see cref="FrameworkPackages"/> per project target framework</param> /// <param name="listPackageArgs">List command args</param> /// <returns>A dictionary where the key is the package id, and the value is a list of <see cref="IPackageSearchMetadata"/>.</returns> internal async Task<Dictionary<string, List<IPackageSearchMetadata>>> GetPackageMetadataAsync( List<FrameworkPackages> targetFrameworks, ListPackageArgs listPackageArgs) { List<string> allPackages = GetAllPackageIdentifiers(targetFrameworks, listPackageArgs.IncludeTransitive); var packageMetadataById = new Dictionary<string, List<IPackageSearchMetadata>>(capacity: allPackages.Count); int maxParallel = listPackageArgs.PackageSources.Any(s => s.IsHttp) ? 8 // Try to be nice to HTTP package sources : listPackageArgs.PackageSources.Count == 0 ? Environment.ProcessorCount + 1 // Fallback when no package sources are configured : (Environment.ProcessorCount / listPackageArgs.PackageSources.Count) + 1; await ThrottledForEachAsync(allPackages, async (packageId, cancellationToken) => await GetPackageMetadataAsync(packageId, listPackageArgs, cancellationToken), packageMetadata => packageMetadataById[packageMetadata.Key] = packageMetadata.Value, maxParallel, listPackageArgs.CancellationToken); return packageMetadataById; static List<string> GetAllPackageIdentifiers(List<FrameworkPackages> frameworks, bool includeTransitive) { IEnumerable<InstalledPackageReference> intermediateEnumerable = frameworks.SelectMany(f => f.TopLevelPackages); if (includeTransitive) { intermediateEnumerable = intermediateEnumerable.Concat(frameworks.SelectMany(f => f.TransitivePackages)); } List<string> allPackages = intermediateEnumerable.Select(p => p.Name).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); return allPackages; } } /// <summary>Run a throttled iteration of a list that performs async work, with a "single threaded" collection of results.</summary> /// <remarks> /// <para>The continuation delegate is called sequentially, so results can be safely added to non-synchronized collections.</para> /// <para>If any task factory invocation throws, or any task faults, the cancellation token will be triggered and the iteration will end early.</para> /// </remarks> /// <typeparam name="TItem">The item type for the input list</typeparam> /// <typeparam name="TResult">The result type of the async work</typeparam> /// <param name="items">The input list to iterate</param> /// <param name="taskFactory">Delegate to start async work.</param> /// <param name="continuation">Delegate with result of async work. Will not be called concurrently.</param> /// <param name="cancellationToken">Cancellation token</param> /// <param name="maxParallel">The maximum number of tasks to allow running in parallel.</param> /// <returns>A task that can be awaited to wait for completion of the iteration.</returns> private async Task ThrottledForEachAsync<TItem, TResult>( IList<TItem> items, Func<TItem, CancellationToken, Task<TResult>> taskFactory, Action<TResult> continuation, int maxParallel, CancellationToken cancellationToken) { int taskCount = Math.Min(items.Count, maxParallel); var tasks = new Task<TResult>[taskCount]; using CancellationTokenSource faultCancelationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); try { // ramp up throttling (fill task array) int itemIndex; for (itemIndex = 0; itemIndex < taskCount; itemIndex++) { tasks[itemIndex] = taskFactory(items[itemIndex], faultCancelationTokenSource.Token); } // throttling steady state (max parallel tasks running, more input items waiting to queue) while (itemIndex < items.Count) { _ = await Task.WhenAny(tasks); for (int i = 0; i < tasks.Length; i++) { if (tasks[i].IsCompleted) { TResult result = await tasks[i]; continuation(result); tasks[i] = taskFactory(items[itemIndex++], faultCancelationTokenSource.Token); break; } } } // ramp down throttling (no more inputs waiting to start, just need to wait for last tasks to finish) await Task.WhenAll(tasks); for (int i = 0; i < tasks.Length; i++) { TResult result = await tasks[i]; continuation(result); } } catch { // Don't leave un-awaited tasks. Request cancellation, then wait for tasks to finish. faultCancelationTokenSource.Cancel(); // Make sure none of the tasks are null (factory exception during ramp-up) for (int i = 0; i < tasks.Length; i++) { if (tasks[i] is null) { tasks[i] = Task.FromResult(default(TResult)); } } await Task.WhenAll(tasks); throw; } finally { faultCancelationTokenSource.Cancel(); } } /// <summary> /// Pre-populate _sourceRepositoryCache so source repository can be reused between different calls. /// </summary> /// <param name="listPackageArgs">List args for the token and source provider</param> private void PopulateSourceRepositoryCache(ListPackageArgs listPackageArgs) { IEnumerable<Lazy<INuGetResourceProvider>> providers = Repository.Provider.GetCoreV3(); IEnumerable<PackageSource> sources = listPackageArgs.PackageSources; foreach (PackageSource source in sources) { SourceRepository sourceRepository = Repository.CreateSource(providers, source, FeedType.Undefined); _sourceRepositoryCache[source] = sourceRepository; } } /// <summary> /// Get last versions for every package from the unique packages /// </summary> /// <param name="frameworks"> List of <see cref="FrameworkPackages"/>.</param> /// <param name="packageMetadata">Package metadata from package sources</param> /// <param name="listPackageArgs">Arguments for list package to get the right latest version</param> internal async Task UpdatePackagesWithSourceMetadata( List<FrameworkPackages> frameworks, Dictionary<string, List<IPackageSearchMetadata>> packageMetadata, ListPackageArgs listPackageArgs) { foreach (var frameworkPackages in frameworks) { foreach (var topLevelPackage in frameworkPackages.TopLevelPackages) { if (packageMetadata.TryGetValue(topLevelPackage.Name, out List<IPackageSearchMetadata> matchingPackage)) { // Get latest metadata *only* if this is a report requiring "outdated" metadata if (listPackageArgs.ReportType == ReportType.Outdated && matchingPackage.Count > 0) { var latestVersion = matchingPackage .Where(package => package.IsListed) .Where(newVersion => MeetsConstraints(newVersion.Identity.Version, topLevelPackage, listPackageArgs)) .Max(i => i.Identity.Version); if (latestVersion is not null) { topLevelPackage.LatestPackageMetadata = matchingPackage.First(p => p.Identity.Version == latestVersion); topLevelPackage.UpdateLevel = GetUpdateLevel(topLevelPackage.ResolvedPackageMetadata.Identity.Version, topLevelPackage.LatestPackageMetadata.Identity.Version); } else // no latest version available with the given constraints { topLevelPackage.LatestPackageMetadata = null; topLevelPackage.UpdateLevel = UpdateLevel.NoUpdate; } } var matchingPackagesWithDeprecationMetadata = await Task.WhenAll( matchingPackage.Select(async v => new { SearchMetadata = v, DeprecationMetadata = await v.GetDeprecationMetadataAsync() })); // Update resolved version with additional metadata information returned by the server. var resolvedVersionFromServer = matchingPackagesWithDeprecationMetadata .FirstOrDefault(v => v.SearchMetadata.Identity.Version == topLevelPackage.ResolvedPackageMetadata.Identity.Version && (v.DeprecationMetadata != null || v.SearchMetadata?.Vulnerabilities != null)); if (resolvedVersionFromServer != null) { topLevelPackage.ResolvedPackageMetadata = resolvedVersionFromServer.SearchMetadata; } } } foreach (var transitivePackage in frameworkPackages.TransitivePackages) { if (packageMetadata.TryGetValue(transitivePackage.Name, out List<IPackageSearchMetadata> matchingPackage)) { // Get latest metadata *only* if this is a report requiring "outdated" metadata if (listPackageArgs.ReportType == ReportType.Outdated && matchingPackage.Count > 0) { var latestVersion = matchingPackage .Where(newVersion => newVersion.IsListed) .Where(newVersion => MeetsConstraints(newVersion.Identity.Version, transitivePackage, listPackageArgs)) .Max(i => i.Identity.Version); transitivePackage.LatestPackageMetadata = matchingPackage.First(p => p.Identity.Version == latestVersion); transitivePackage.UpdateLevel = GetUpdateLevel(transitivePackage.ResolvedPackageMetadata.Identity.Version, transitivePackage.LatestPackageMetadata.Identity.Version); } var matchingPackagesWithDeprecationMetadata = await Task.WhenAll( matchingPackage.Select(async v => new { SearchMetadata = v, DeprecationMetadata = await v.GetDeprecationMetadataAsync() })); // Update resolved version with additional metadata information returned by the server. var resolvedVersionFromServer = matchingPackagesWithDeprecationMetadata .FirstOrDefault(v => v.SearchMetadata.Identity.Version == transitivePackage.ResolvedPackageMetadata.Identity.Version && (v.DeprecationMetadata != null || v.SearchMetadata?.Vulnerabilities != null)); if (resolvedVersionFromServer != null) { transitivePackage.ResolvedPackageMetadata = resolvedVersionFromServer.SearchMetadata; } } } } } /// <summary> /// Update Level is used to determine the print color for the latest /// version, which depends on changing major, minor or patch /// </summary> /// <param name="resolvedVersion"> Package's resolved version </param> /// <param name="latestVersion"> Package's latest version </param> /// <returns></returns> private UpdateLevel GetUpdateLevel(NuGetVersion resolvedVersion, NuGetVersion latestVersion) { if (latestVersion == null) return UpdateLevel.NoUpdate; if (resolvedVersion.Major != latestVersion.Major) { return UpdateLevel.Major; } else if (resolvedVersion.Minor != latestVersion.Minor) { return UpdateLevel.Minor; } //Patch or less important version props are different else if (resolvedVersion != latestVersion) { return UpdateLevel.Patch; } return UpdateLevel.NoUpdate; } /// <summary> /// Gets the package metadata for a specific package from all sources /// </summary> /// <param name="package">The package to get the latest version for</param> /// <param name="listPackageArgs">List args for the token and source provider></param> /// <param name="cancellationToken"></param> /// <returns>A list of tasks for all latest versions for packages from all sources</returns> private async Task<KeyValuePair<string, List<IPackageSearchMetadata>>> GetPackageMetadataAsync( string package, ListPackageArgs listPackageArgs, CancellationToken cancellationToken) { var results = new List<IPackageSearchMetadata>(); var sources = listPackageArgs.PackageSources; await ThrottledForEachAsync(sources, async (source, innerCancellationToken) => await GetPackageMetadataAsync(source, listPackageArgs, package, innerCancellationToken), continuation: results.AddRange, maxParallel: listPackageArgs.PackageSources.Count, cancellationToken); return new KeyValuePair<string, List<IPackageSearchMetadata>>(package, results); } /// <summary> /// Gets package metadata for a specific package from a specific source /// </summary> /// <param name="packageSource">The source to look for packages at</param> /// <param name="listPackageArgs">The list args for the cancellation token</param> /// <param name="package">Package to look for updates for</param> /// <param name="cancellationToken"></param> /// <returns>An updated package with the highest version at a single source</returns> private async Task<IEnumerable<IPackageSearchMetadata>> GetPackageMetadataAsync( PackageSource packageSource, ListPackageArgs listPackageArgs, string package, CancellationToken cancellationToken) { SourceRepository sourceRepository = _sourceRepositoryCache[packageSource]; var packageMetadataResource = await sourceRepository.GetResourceAsync<PackageMetadataResource>(cancellationToken); using var sourceCacheContext = new SourceCacheContext(); // This is used for --outdated, --deprecated, and --vulnerable. // So, we need the package metadata for the currently installed version, // even if it's pre-release or unlisted. // This means that the logic for --outdated has to do filtering based on IsListed and // prerelease. IEnumerable<IPackageSearchMetadata> packages = await packageMetadataResource.GetMetadataAsync( package, includePrerelease: true, includeUnlisted: true, sourceCacheContext: sourceCacheContext, log: listPackageArgs.Logger, token: listPackageArgs.CancellationToken); return packages; } /// <summary> /// Given a found version from a source and the current version and the args /// of list package, this function checks if the found version meets the required /// highest-patch, highest-minor or prerelease /// </summary> /// <param name="newVersion">Version from a source</param> /// <param name="package">The required package with its current version</param> /// <param name="listPackageArgs">Used to get the constraints</param> /// <returns>Whether the new version meets the constraints or not</returns> private bool MeetsConstraints(NuGetVersion newVersion, InstalledPackageReference package, ListPackageArgs listPackageArgs) { var result = !newVersion.IsPrerelease || listPackageArgs.Prerelease; if (listPackageArgs.HighestPatch) { result = newVersion.Minor.Equals(package.ResolvedPackageMetadata.Identity.Version.Minor) && newVersion.Major.Equals(package.ResolvedPackageMetadata.Identity.Version.Major) && result; } if (listPackageArgs.HighestMinor) { result = newVersion.Major.Equals(package.ResolvedPackageMetadata.Identity.Version.Major) && result; } return result; } } } |