Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/code/PSResourceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,18 @@ public static void WritePSGetResourceInfo(

throw new PSArgumentException("psObjectGetInfo argument is not a PSGetResourceInfo type.");
}

public static string SelectV3PackageContentUrl(
string[] versionedResponses,
string version)
{
if (!NuGetVersion.TryParse(version, out NuGetVersion requiredVersion))
{
throw new PSArgumentException($"Version '{version}' is not a valid NuGet version.");
}

return Cmdlets.V3ServerAPICalls.GetPackageContentUrlForVersion(versionedResponses, requiredVersion);
}
}

#endregion
Expand Down
129 changes: 107 additions & 22 deletions src/code/V3ServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -913,17 +913,7 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error
}
else
{
// loop through responses to find one containing required version
foreach (string response in versionedResponses)
{
// Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg"
// Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg
if (response.Contains(version.ToNormalizedString()))
{
pkgContentUrl = response;
break;
}
}
pkgContentUrl = GetPackageContentUrlForVersion(versionedResponses, version);
}

if (String.IsNullOrEmpty(pkgContentUrl))
Expand Down Expand Up @@ -997,17 +987,7 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
}
else
{
// loop through responses to find one containing required version
foreach (string response in versionedResponses)
{
// Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg"
// Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg
if (response.Contains(version.ToNormalizedString()))
{
pkgContentUrl = response;
break;
}
}
pkgContentUrl = GetPackageContentUrlForVersion(versionedResponses, version);
}

if (String.IsNullOrEmpty(pkgContentUrl))
Expand Down Expand Up @@ -1039,6 +1019,111 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
return pkgStream;
}

/// <summary>
/// Selects the "packageContent" entry (i.e the .nupkg download URL) matching the required version.
/// The version encoded in the entry is parsed and compared as a NuGetVersion, instead of searching for the version
/// text anywhere within the entry, as a substring search matches version prefixes too
/// (i.e requesting version '1.2.3' would match the entry for version '1.2.30').
/// </summary>
internal static string GetPackageContentUrlForVersion(string[] versionedResponses, NuGetVersion requiredVersion)
{
if (versionedResponses == null || requiredVersion == null)
{
return String.Empty;
}

foreach (string response in versionedResponses)
{
if (String.IsNullOrWhiteSpace(response))
{
continue;
}

// Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg"
// Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg
if (PackageContentUrlMatchesVersion(response, requiredVersion))
{
return response;
}
}

return String.Empty;
}

/// <summary>
/// Determines whether the given "packageContent" entry refers to the required version.
/// </summary>
private static bool PackageContentUrlMatchesVersion(string packageContentUrl, NuGetVersion requiredVersion)
{
string path = packageContentUrl;
string query = String.Empty;
int queryIndex = path.IndexOfAny(new char[] { '?', '#' });
if (queryIndex >= 0)
{
query = path.Substring(queryIndex + 1);
path = path.Substring(0, queryIndex);
}

string[] pathSegments = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
string normalizedVersion = requiredVersion.ToNormalizedString();
for (int i = 0; i < pathSegments.Length; i++)
{
string segment = UnescapeUrlPart(pathSegments[i]);

// Path segment containing just the version, ex: ".../test_module/1.0.0/..."
if (NuGetVersion.TryParse(segment, out NuGetVersion segmentVersion) && segmentVersion == requiredVersion)
{
return true;
}

// Last path segment is the file name, ex: "test_module.1.0.0.nupkg"
if (segment.EndsWith($".{normalizedVersion}.nupkg", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

// Some repositories pass the version as a query parameter, ex: "...?packageVersion=1.0.0"
foreach (string queryParameter in query.Split(new char[] { '&', ';' }, StringSplitOptions.RemoveEmptyEntries))
{
int separatorIndex = queryParameter.IndexOf('=');
if (separatorIndex < 0)
{
continue;
}

string queryKey = UnescapeUrlPart(queryParameter.Substring(0, separatorIndex)).Trim();
if (!queryKey.EndsWith("version", StringComparison.OrdinalIgnoreCase))
{
continue;
}

string queryValue = UnescapeUrlPart(queryParameter.Substring(separatorIndex + 1));
if (NuGetVersion.TryParse(queryValue, out NuGetVersion queryVersion) && queryVersion == requiredVersion)
{
return true;
}
}

return false;
}

private static string UnescapeUrlPart(string urlPart)
{
try
{
return Uri.UnescapeDataString(urlPart);
}
catch (UriFormatException)
{
return urlPart;
}
catch (ArgumentException)
{
return urlPart;
}
}

/// <summary>
/// Gets the versioned package entries from the RegistrationsBaseUrl resource
/// i.e when the package Name being searched for does not contain wildcard
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Import-Module "$psscriptroot/../PSGetTestUtils.psm1" -Force

Describe 'Test V3 packageContent url selection for a required version' -tags 'CI' {

BeforeAll {
$packageBaseAddress = 'https://api.nuget.org/v3-flatcontainer/test_module'
# Responses are returned in descending version order, ie the entry for 1.2.30 precedes the entry for 1.2.3
$versionedResponses = @(
"$packageBaseAddress/1.2.30/test_module.1.2.30.nupkg",
"$packageBaseAddress/1.2.3/test_module.1.2.3.nupkg"
)
}

It 'Should select the url for the exact version requested' {
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($versionedResponses, '1.2.3')
$url | Should -BeExactly "$packageBaseAddress/1.2.3/test_module.1.2.3.nupkg"
}

It 'Should select the url for a version which another version is a prefix of' {
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($versionedResponses, '1.2.30')
$url | Should -BeExactly "$packageBaseAddress/1.2.30/test_module.1.2.30.nupkg"
}

It 'Should select the url for a version with four version parts' {
$responses = @(
"$packageBaseAddress/2024.5.20.12/test_module.2024.5.20.12.nupkg",
"$packageBaseAddress/2024.5.20.1/test_module.2024.5.20.1.nupkg"
)
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '2024.5.20.1')
$url | Should -BeExactly "$packageBaseAddress/2024.5.20.1/test_module.2024.5.20.1.nupkg"
}

It 'Should select the url for a prerelease version' {
$responses = @(
"$packageBaseAddress/2.5.0-beta10/test_module.2.5.0-beta10.nupkg",
"$packageBaseAddress/2.5.0-beta1/test_module.2.5.0-beta1.nupkg"
)
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '2.5.0-beta1')
$url | Should -BeExactly "$packageBaseAddress/2.5.0-beta1/test_module.2.5.0-beta1.nupkg"
}

It 'Should select the url when the version is passed as a query parameter' {
$responses = @(
"https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.30",
"https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.3"
)
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '1.2.3')
$url | Should -BeExactly "https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.3"
}

It 'Should not select a url where a non-version query parameter matches the version' {
$responses = @(
"https://www.myget.org/api/download?packageId=1.2.3&packageVersion=1.2.30",
"https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.3"
)
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '1.2.3')
$url | Should -BeExactly "https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.3"
}

It 'Should not select any url when the requested version is not present' {
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($versionedResponses, '1.2.4')
$url | Should -BeNullOrEmpty
}
}