Skip to content

Commit c8b8de4

Browse files
committed
Add package source mapping
1 parent 14332c6 commit c8b8de4

File tree

3 files changed

+237
-7
lines changed

3 files changed

+237
-7
lines changed

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,18 @@ public static async Task<int> RunAsync(PackageDownloadArgs args, CancellationTok
4848

4949
public static async Task<int> RunAsync(PackageDownloadArgs args, ILoggerWithColor logger, IReadOnlyList<PackageSource> packageSources, ISettings settings, CancellationToken token)
5050
{
51-
// Check for insecure sources
52-
if (DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger))
51+
// If --source is explicitly provided, validate those sources up front.
52+
if (args.Sources != null && args.Sources.Count > 0 && DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger))
5353
{
5454
return ExitCodeError;
5555
}
5656

5757
string outputDirectory = args.OutputDirectory ?? Directory.GetCurrentDirectory();
5858
var cache = new SourceCacheContext();
59-
IReadOnlyList<SourceRepository> sourceRepositories = GetSourceRepositories(packageSources);
59+
60+
IReadOnlyDictionary<string, SourceRepository> sourceRepositoriesMap = GetSourceRepositories(packageSources);
61+
var packageSourceMapping = PackageSourceMapping.GetPackageSourceMapping(settings);
62+
6063
bool downloadedAllSuccessfully = true;
6164

6265
foreach (var package in args.Packages)
@@ -67,6 +70,19 @@ public static async Task<int> RunAsync(PackageDownloadArgs args, ILoggerWithColo
6770
package.Id,
6871
string.IsNullOrEmpty(package.NuGetVersion?.ToNormalizedString()) ? Strings.PackageDownloadCommand_LatestVersion : package.NuGetVersion.ToNormalizedString()));
6972

73+
// Resolve which repositories to use for this package
74+
if (!TryGetRepositoriesForPackage(
75+
package.Id,
76+
args,
77+
packageSources,
78+
packageSourceMapping,
79+
sourceRepositoriesMap,
80+
logger,
81+
out List<SourceRepository> sourceRepositories))
82+
{
83+
return ExitCodeError;
84+
}
85+
7086
try
7187
{
7288
(NuGetVersion version, SourceRepository downloadRepository) =
@@ -188,6 +204,50 @@ await ResolvePackageDownloadVersion(
188204
return (versionToDownload, downloadSourceRepository);
189205
}
190206

207+
/// <summary>
208+
/// Builds the set of SourceRepository objects to use for a given package,
209+
/// applying package source mapping (when --source is not provided) and
210+
/// validating HTTP usage only on the *effective* sources.
211+
/// </summary>
212+
private static bool TryGetRepositoriesForPackage(
213+
string packageId,
214+
PackageDownloadArgs args,
215+
IReadOnlyList<PackageSource> allPackageSources,
216+
PackageSourceMapping packageSourceMapping,
217+
IReadOnlyDictionary<string, SourceRepository> sourceRepositoriesMap,
218+
ILoggerWithColor logger,
219+
out List<SourceRepository> repositories)
220+
{
221+
repositories = new List<SourceRepository>();
222+
223+
if (args.Sources != null && args.Sources.Count > 0)
224+
{
225+
// --source specified: use the provided sources
226+
foreach (var source in allPackageSources)
227+
{
228+
repositories.Add(sourceRepositoriesMap[source.Name]);
229+
}
230+
231+
return true;
232+
}
233+
234+
// No --source: use package source mapping
235+
var mappedSourceNames = packageSourceMapping.GetConfiguredPackageSources(packageId);
236+
var mappedSources = allPackageSources.Where(ps => mappedSourceNames.Contains(ps.Name, StringComparer.OrdinalIgnoreCase)).ToList();
237+
if (DetectAndReportInsecureSources(args.AllowInsecureConnections, mappedSources, logger))
238+
{
239+
return false;
240+
}
241+
242+
foreach (var name in mappedSourceNames)
243+
{
244+
repositories.Add(sourceRepositoriesMap[name]);
245+
}
246+
247+
return true;
248+
}
249+
250+
191251
private static async Task<bool> DownloadPackageAsync(
192252
string id,
193253
NuGetVersion version,
@@ -271,13 +331,13 @@ private static bool DetectAndReportInsecureSources(
271331
return false;
272332
}
273333

274-
private static IReadOnlyList<SourceRepository> GetSourceRepositories(IReadOnlyList<PackageSource> packageSources)
334+
private static IReadOnlyDictionary<string, SourceRepository> GetSourceRepositories(IReadOnlyList<PackageSource> packageSources)
275335
{
276336
IEnumerable<Lazy<INuGetResourceProvider>> providers = Repository.Provider.GetCoreV3();
277-
List<SourceRepository> sourceRepositories = [];
337+
Dictionary<string, SourceRepository> sourceRepositories = [];
278338
foreach (var source in packageSources)
279339
{
280-
sourceRepositories.Add(Repository.CreateSource(providers, source, FeedType.Undefined));
340+
sourceRepositories[source.Name] = Repository.CreateSource(providers, source, FeedType.Undefined);
281341
}
282342

283343
return sourceRepositories;

test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using FluentAssertions;
11+
using global::Test.Utility;
1112
using Moq;
1213
using NuGet.CommandLine.XPlat;
1314
using NuGet.CommandLine.XPlat.Commands.Package;
@@ -426,4 +427,174 @@ [new PackageSource(context.PackageSource)],
426427
File.Exists(Path.Combine(context.WorkingDirectory, $"{id.ToLowerInvariant()}.{v}.nupkg"))
427428
.Should().BeFalse("Package does not exist in sources");
428429
}
430+
431+
public static IEnumerable<object[]> Cases()
432+
{
433+
// Parameters:
434+
// A-packages, B-packages, sourceMappings, sourcesArgs, downloadId, downloadVersion,
435+
// allowInsecureConnections, expectSuccess, expectedInstalled
436+
437+
// --source specified, mapping ignored, package only in A -> success
438+
yield return new object[]
439+
{
440+
new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A
441+
new List<(string,string)>(), // B
442+
new List<(string,string)> { ("B", "Contoso.*") }, // mapping ignored
443+
new List<string> { "A" }, // --source A
444+
"Contoso.Lib", "1.0.0", // downloadId, downloadVersion
445+
true, // allow insecure
446+
true, // expect success
447+
("Contoso.Lib", "1.0.0") // expectedInstalled
448+
};
449+
450+
// no --source, mapping -> B, package only in B -> success
451+
yield return new object[]
452+
{
453+
new List<(string,string)>(), // A
454+
new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, // B
455+
new List<(string,string)> { ("B", "Contoso.*") }, // mapping -> B
456+
null, // no --source
457+
"Contoso.Mapped", "2.0.0", // downloadId, downloadVersion
458+
true, // allow insecure
459+
true, // expect success
460+
("Contoso.Mapped", "2.0.0") // expectedInstalled
461+
};
462+
463+
// no --source, mapping -> A, package only in B -> fail
464+
yield return new object[]
465+
{
466+
new List<(string,string)>(), // A
467+
new List<(string,string)> { ("Contoso.Mapped", "2.0.0") },
468+
new List<(string,string)> { ("A", "Contoso.*") }, // mapped to A
469+
null,
470+
"Contoso.Mapped", "2.0.0",
471+
true,
472+
false,
473+
null!
474+
};
475+
476+
// --source specified, no source mapping with an insecure source
477+
yield return new object[]
478+
{
479+
new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A
480+
new List<(string,string)>(),
481+
new List<(string,string)> { ("A", "Contoso.*") },
482+
new List<string> { "A" }, // --source
483+
"Contoso.Lib", "1.0.0",
484+
false, // allow insecure connections false / not set to true
485+
false,
486+
null!
487+
};
488+
489+
// no --source, mapping -> B, allow insecure not enabled -> fail
490+
yield return new object[]
491+
{
492+
new List<(string,string)>(), // A
493+
new List<(string,string)> { ("Contoso.Mapped", "1.0.0") },
494+
new List<(string,string)> { ("B", "Contoso.*") },
495+
null,
496+
"Contoso.Mapped", "1.0.0",
497+
false, // allow insecure connections false / not set to true
498+
false,
499+
null!
500+
};
501+
}
502+
503+
[Theory]
504+
[MemberData(nameof(Cases))]
505+
public async Task RunAsync_WithSourceMapping_ListDriven_UsingCleanSetup(
506+
IReadOnlyList<(string id, string version)> sourceAPackages,
507+
IReadOnlyList<(string id, string version)> sourceBPackages,
508+
IReadOnlyList<(string source, string pattern)> sourceMappings,
509+
IReadOnlyList<string> sourcesArgs,
510+
string downloadId,
511+
string downloadVersion,
512+
bool allowInsecureConnections,
513+
bool expectSuccess,
514+
(string id, string version)? expectedInstalled)
515+
{
516+
// Arrange
517+
using var context = new SimpleTestPathContext();
518+
string srcADirectory = Path.Combine(context.PackageSource, "SourceA");
519+
string srcBDirectory = Path.Combine(context.PackageSource, "SourceB");
520+
521+
using var serverA = new FileSystemBackedV3MockServer(srcADirectory);
522+
using var serverB = new FileSystemBackedV3MockServer(srcBDirectory);
523+
524+
foreach (var (id, ver) in sourceAPackages)
525+
{
526+
await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, id, ver);
527+
}
528+
529+
foreach (var (id, ver) in sourceBPackages)
530+
{
531+
await SimpleTestPackageUtility.CreateFullPackageAsync(srcBDirectory, id, ver);
532+
}
533+
534+
serverA.Start();
535+
serverB.Start();
536+
537+
// sources
538+
context.Settings.AddSource("A", serverA.ServiceIndexUri);
539+
context.Settings.AddSource("B", serverB.ServiceIndexUri);
540+
541+
// mapping
542+
foreach (var (src, pattern) in sourceMappings)
543+
{
544+
context.Settings.AddPackageSourceMapping(src, pattern);
545+
}
546+
547+
var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]);
548+
549+
var packageSources = new List<PackageSource>
550+
{
551+
new(serverA.ServiceIndexUri, "A"),
552+
new(serverB.ServiceIndexUri, "B")
553+
};
554+
555+
// args
556+
var args = new PackageDownloadArgs
557+
{
558+
Packages =
559+
[
560+
new PackageWithNuGetVersion
561+
{
562+
Id = downloadId,
563+
NuGetVersion = downloadVersion is null ? null : NuGetVersion.Parse(downloadVersion)
564+
}
565+
],
566+
OutputDirectory = context.WorkingDirectory,
567+
AllowInsecureConnections = allowInsecureConnections,
568+
Sources = sourcesArgs == null ? [] : sourcesArgs.ToList()
569+
};
570+
571+
var logger = new Mock<ILoggerWithColor>(MockBehavior.Loose);
572+
573+
// Act
574+
var exit = await PackageDownloadRunner.RunAsync(
575+
args,
576+
logger.Object,
577+
packageSources,
578+
settings,
579+
CancellationToken.None);
580+
581+
serverA.Stop();
582+
serverB.Stop();
583+
584+
// Assert
585+
if (expectSuccess)
586+
{
587+
exit.Should().Be(PackageDownloadRunner.ExitCodeSuccess);
588+
expectedInstalled.Should().NotBeNull();
589+
590+
var (expId, expVer) = expectedInstalled!.Value;
591+
var installDir = Path.Combine(context.WorkingDirectory, expId.ToLowerInvariant(), expVer);
592+
Directory.Exists(installDir).Should().BeTrue();
593+
File.Exists(Path.Combine(installDir, $"{expId.ToLowerInvariant()}.{expVer}.nupkg")).Should().BeTrue();
594+
}
595+
else
596+
{
597+
exit.Should().Be(PackageDownloadRunner.ExitCodeError);
598+
}
599+
}
429600
}

test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,4 @@ public async Task ResolvePackageDownloadVersion_UnlistedPackage_BehavesAsExpecte
200200
foundRepo.Should().BeNull();
201201
}
202202
}
203-
204203
}

0 commit comments

Comments
 (0)