| File: Packaging\PackageSourceOverrideMappings.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.Utils; namespace Aspire.Cli.Packaging; internal static class PackageSourceOverrideMappings { internal const string DefaultPackagePattern = "Aspire*"; /// <summary> /// Resolves a command-line package source against the invocation directory, returning relative local sources as absolute paths so persisted mappings remain valid elsewhere. /// </summary> public static string ResolveForWorkingDirectory(string source, DirectoryInfo workingDirectory) { ArgumentException.ThrowIfNullOrWhiteSpace(source); ArgumentNullException.ThrowIfNull(workingDirectory); var sourceKind = ClassifySource(source, out _); // On Unix, Uri treats DOS-shaped paths such as C:/feed as absolute file URIs. // Preserve a file URI only when the source explicitly includes the file: scheme. if (Path.IsPathFullyQualified(source) || sourceKind is PackageSourceKind.Http or PackageSourceKind.FileUri) { return source; } return Path.GetFullPath(source, workingDirectory.FullName); } public static string? GetMissingLocalDirectory(string source) { ArgumentException.ThrowIfNullOrWhiteSpace(source); var sourceKind = ClassifySource(source, out var localDirectory); if (sourceKind is PackageSourceKind.Http) { return null; } return Directory.Exists(localDirectory) ? null : localDirectory; } public static PackageMapping[] Create( string packageSourceOverride, PackageChannel? requestedChannel, string? nugetServiceIndexOverride, string? packagePattern = null) { ArgumentException.ThrowIfNullOrWhiteSpace(packageSourceOverride); ThrowIfCredentialBearingSourceOverride(packageSourceOverride); packagePattern = GetEffectivePackagePattern(packagePattern); var mappings = new List<PackageMapping> { new(packagePattern, packageSourceOverride) }; if (IsExactPackagePattern(packagePattern)) { // The exact pattern guarantees that the selected integration comes from --source. // Keep that source eligible for the remaining Aspire closure alongside the resolved // channel or ambient feed. NuGet has no source priority, so matching packages can come // from either feed; the exact selected-package pattern remains authoritative. mappings.Add(new PackageMapping(DefaultPackagePattern, packageSourceOverride)); mappings.Add(new PackageMapping(PackageMapping.AllPackages, packageSourceOverride)); } if (requestedChannel?.Mappings is not null) { foreach (var mapping in requestedChannel.Mappings) { if (CompetesWithAuthoritativePattern(mapping.PackageFilter, packagePattern)) { continue; } mappings.Add(mapping); } } if (!mappings.Any(mapping => mapping.PackageFilter == PackageMapping.AllPackages && !PackageSourceIdentity.Comparer.Equals(mapping.Source, packageSourceOverride))) { // Honor the runtime service-index override (env / sidecar) when the // CLI emits a fresh fallback mapping. Reads from existing user // configs are not rewritten — see docs/specs/cli-identity-sidecar.md. var fallbackSource = string.IsNullOrEmpty(nugetServiceIndexOverride) ? PackageSources.NuGetOrg : nugetServiceIndexOverride; mappings.Add(new PackageMapping(PackageMapping.AllPackages, fallbackSource)); } // Equivalent mappings must not select different isolated restore caches. Preserve the // first source spelling, but deduplicate using the same identity as alias resolution. return [ .. mappings.DistinctBy(static mapping => (mapping.PackageFilter.ToUpperInvariant(), PackageSourceIdentity.Normalize(mapping.Source))) ]; } internal static string GetEffectivePackagePattern(string? packagePattern) => string.IsNullOrWhiteSpace(packagePattern) ? DefaultPackagePattern : packagePattern; internal static bool IsExactPackagePattern(string packagePattern) => !packagePattern.EndsWith('*'); internal static bool MatchesPackage(string packagePattern, string packageName) => packagePattern.EndsWith('*') ? packageName.StartsWith(packagePattern[..^1], StringComparison.OrdinalIgnoreCase) : string.Equals(packageName, packagePattern, StringComparison.OrdinalIgnoreCase); internal static bool CompetesWithAuthoritativePattern( string candidatePattern, string authoritativePattern) { if (authoritativePattern == PackageMapping.AllPackages) { return false; } if (!authoritativePattern.EndsWith('*')) { return string.Equals(candidatePattern, authoritativePattern, StringComparison.OrdinalIgnoreCase); } var authoritativePrefix = authoritativePattern[..^1]; if (candidatePattern == PackageMapping.AllPackages) { return false; } var candidatePrefix = candidatePattern.EndsWith('*') ? candidatePattern[..^1] : candidatePattern; return candidatePrefix.Length >= authoritativePrefix.Length && candidatePrefix.StartsWith(authoritativePrefix, StringComparison.OrdinalIgnoreCase); } public static PackageMapping[] CreateForSourceOnlyOperations(string packageSourceOverride) { ArgumentException.ThrowIfNullOrWhiteSpace(packageSourceOverride); ThrowIfCredentialBearingSourceOverride(packageSourceOverride); // NuGet package search queries every configured source without applying package source // mapping. Keep the temporary config exclusive to --source so discovery and installation // cannot contact a channel feed or NuGet.org behind the user's approved proxy. return [ new(DefaultPackagePattern, packageSourceOverride), new(PackageMapping.AllPackages, packageSourceOverride) ]; } public static bool HasCredentialMaterial(string source) => NuGetSourceIdentity.HasCredentialMaterial(source); private static void ThrowIfCredentialBearingSourceOverride(string source) { if (HasCredentialMaterial(source)) { throw new ArgumentException( "Credential-bearing HTTP sources cannot be supplied through --source. Configure credentials through NuGet instead.", nameof(source)); } } public static string? GetNormalizedLocalDirectory(string source) { if (string.IsNullOrWhiteSpace(source)) { return null; } var trimmedSource = source.Trim(); if (UrlHelper.IsHttpUrl(trimmedSource)) { return null; } try { if (Uri.TryCreate(trimmedSource, UriKind.Absolute, out var uri)) { return uri.IsFile ? Path.GetFullPath(uri.LocalPath) : null; } return Path.GetFullPath(trimmedSource); } catch (ArgumentException) { return null; } catch (NotSupportedException) { return null; } catch (PathTooLongException) { return null; } catch (UriFormatException) { return null; } } private static PackageSourceKind ClassifySource(string source, out string? localDirectory) { var trimmedSource = source.Trim(); if (trimmedSource.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || trimmedSource.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { localDirectory = null; return PackageSourceKind.Http; } if (source.StartsWith("file:", StringComparison.OrdinalIgnoreCase) && Uri.TryCreate(source, UriKind.Absolute, out var uri) && uri.IsFile) { localDirectory = uri.LocalPath; return PackageSourceKind.FileUri; } localDirectory = source; return PackageSourceKind.LocalPath; } private enum PackageSourceKind { Http, FileUri, LocalPath } }