Skip to content

Commit 66aecec

Browse files
Copilotet1975
andauthored
Fix V3 packageContent URL selection to compare parsed versions (issue 1657)
Co-authored-by: et1975 <623703+et1975@users.noreply.github.com>
1 parent 3f0d2e5 commit 66aecec

3 files changed

Lines changed: 167 additions & 22 deletions

File tree

‎src/code/PSResourceInfo.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,6 +2023,18 @@ public static void WritePSGetResourceInfo(
20232023

20242024
throw new PSArgumentException("psObjectGetInfo argument is not a PSGetResourceInfo type.");
20252025
}
2026+
2027+
public static string SelectV3PackageContentUrl(
2028+
string[] versionedResponses,
2029+
string version)
2030+
{
2031+
if (!NuGetVersion.TryParse(version, out NuGetVersion requiredVersion))
2032+
{
2033+
throw new PSArgumentException($"Version {version} is not a valid NuGet version.");
2034+
}
2035+
2036+
return Cmdlets.V3ServerAPICalls.GetPackageContentUrlForVersion(versionedResponses, requiredVersion);
2037+
}
20262038
}
20272039

20282040
#endregion

‎src/code/V3ServerAPICalls.cs‎

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -913,17 +913,7 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error
913913
}
914914
else
915915
{
916-
// loop through responses to find one containing required version
917-
foreach (string response in versionedResponses)
918-
{
919-
// Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg"
920-
// Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg
921-
if (response.Contains(version.ToNormalizedString()))
922-
{
923-
pkgContentUrl = response;
924-
break;
925-
}
926-
}
916+
pkgContentUrl = GetPackageContentUrlForVersion(versionedResponses, version);
927917
}
928918

929919
if (String.IsNullOrEmpty(pkgContentUrl))
@@ -997,17 +987,7 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
997987
}
998988
else
999989
{
1000-
// loop through responses to find one containing required version
1001-
foreach (string response in versionedResponses)
1002-
{
1003-
// Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg"
1004-
// Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg
1005-
if (response.Contains(version.ToNormalizedString()))
1006-
{
1007-
pkgContentUrl = response;
1008-
break;
1009-
}
1010-
}
990+
pkgContentUrl = GetPackageContentUrlForVersion(versionedResponses, version);
1011991
}
1012992

1013993
if (String.IsNullOrEmpty(pkgContentUrl))
@@ -1039,6 +1019,101 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
10391019
return pkgStream;
10401020
}
10411021

1022+
/// <summary>
1023+
/// Selects the "packageContent" entry (i.e the .nupkg download URL) matching the required version.
1024+
/// The version encoded in the entry is parsed and compared as a NuGetVersion, instead of searching for the version
1025+
/// text anywhere within the entry, as a substring search matches version prefixes too
1026+
/// (i.e requesting version '1.2.3' would match the entry for version '1.2.30').
1027+
/// </summary>
1028+
internal static string GetPackageContentUrlForVersion(string[] versionedResponses, NuGetVersion requiredVersion)
1029+
{
1030+
if (versionedResponses == null || requiredVersion == null)
1031+
{
1032+
return String.Empty;
1033+
}
1034+
1035+
foreach (string response in versionedResponses)
1036+
{
1037+
if (String.IsNullOrWhiteSpace(response))
1038+
{
1039+
continue;
1040+
}
1041+
1042+
// Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg"
1043+
// Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg
1044+
if (PackageContentUrlMatchesVersion(response, requiredVersion))
1045+
{
1046+
return response;
1047+
}
1048+
}
1049+
1050+
return String.Empty;
1051+
}
1052+
1053+
/// <summary>
1054+
/// Determines whether the given "packageContent" entry refers to the required version.
1055+
/// </summary>
1056+
private static bool PackageContentUrlMatchesVersion(string packageContentUrl, NuGetVersion requiredVersion)
1057+
{
1058+
string path = packageContentUrl;
1059+
string query = String.Empty;
1060+
int queryIndex = path.IndexOfAny(new char[] { '?', '#' });
1061+
if (queryIndex >= 0)
1062+
{
1063+
query = path.Substring(queryIndex + 1);
1064+
path = path.Substring(0, queryIndex);
1065+
}
1066+
1067+
string[] pathSegments = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
1068+
string normalizedVersion = requiredVersion.ToNormalizedString();
1069+
for (int i = 0; i < pathSegments.Length; i++)
1070+
{
1071+
string segment = UnescapeUrlPart(pathSegments[i]);
1072+
1073+
// Path segment containing just the version, ex: ".../test_module/1.0.0/..."
1074+
if (NuGetVersion.TryParse(segment, out NuGetVersion segmentVersion) && segmentVersion == requiredVersion)
1075+
{
1076+
return true;
1077+
}
1078+
1079+
// Last path segment is the file name, ex: "test_module.1.0.0.nupkg"
1080+
if (segment.EndsWith($".{normalizedVersion}.nupkg", StringComparison.OrdinalIgnoreCase))
1081+
{
1082+
return true;
1083+
}
1084+
}
1085+
1086+
// Some repositories pass the version as a query parameter, ex: "...?packageVersion=1.0.0"
1087+
foreach (string queryParameter in query.Split(new char[] { '&', ';' }, StringSplitOptions.RemoveEmptyEntries))
1088+
{
1089+
int separatorIndex = queryParameter.IndexOf('=');
1090+
if (separatorIndex < 0)
1091+
{
1092+
continue;
1093+
}
1094+
1095+
string queryValue = UnescapeUrlPart(queryParameter.Substring(separatorIndex + 1));
1096+
if (NuGetVersion.TryParse(queryValue, out NuGetVersion queryVersion) && queryVersion == requiredVersion)
1097+
{
1098+
return true;
1099+
}
1100+
}
1101+
1102+
return false;
1103+
}
1104+
1105+
private static string UnescapeUrlPart(string urlPart)
1106+
{
1107+
try
1108+
{
1109+
return Uri.UnescapeDataString(urlPart);
1110+
}
1111+
catch (UriFormatException)
1112+
{
1113+
return urlPart;
1114+
}
1115+
}
1116+
10421117
/// <summary>
10431118
/// Gets the versioned package entries from the RegistrationsBaseUrl resource
10441119
/// i.e when the package Name being searched for does not contain wildcard
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Import-Module "$psscriptroot/../PSGetTestUtils.psm1" -Force
5+
6+
Describe 'Test V3 packageContent url selection for a required version' -tags 'CI' {
7+
8+
BeforeAll {
9+
$packageBaseAddress = 'https://api.nuget.org/v3-flatcontainer/test_module'
10+
# Responses are returned in descending version order, ie the entry for 1.2.30 precedes the entry for 1.2.3
11+
$versionedResponses = @(
12+
"$packageBaseAddress/1.2.30/test_module.1.2.30.nupkg",
13+
"$packageBaseAddress/1.2.3/test_module.1.2.3.nupkg"
14+
)
15+
}
16+
17+
It 'Should select the url for the exact version requested' {
18+
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($versionedResponses, '1.2.3')
19+
$url | Should -BeExactly "$packageBaseAddress/1.2.3/test_module.1.2.3.nupkg"
20+
}
21+
22+
It 'Should not select the url of a version which the requested version is a prefix of' {
23+
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($versionedResponses, '1.2.30')
24+
$url | Should -BeExactly "$packageBaseAddress/1.2.30/test_module.1.2.30.nupkg"
25+
}
26+
27+
It 'Should select the url for a version with four version parts' {
28+
$responses = @(
29+
"$packageBaseAddress/2024.5.20.12/test_module.2024.5.20.12.nupkg",
30+
"$packageBaseAddress/2024.5.20.1/test_module.2024.5.20.1.nupkg"
31+
)
32+
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '2024.5.20.1')
33+
$url | Should -BeExactly "$packageBaseAddress/2024.5.20.1/test_module.2024.5.20.1.nupkg"
34+
}
35+
36+
It 'Should select the url for a prerelease version' {
37+
$responses = @(
38+
"$packageBaseAddress/2.5.0-beta10/test_module.2.5.0-beta10.nupkg",
39+
"$packageBaseAddress/2.5.0-beta1/test_module.2.5.0-beta1.nupkg"
40+
)
41+
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '2.5.0-beta1')
42+
$url | Should -BeExactly "$packageBaseAddress/2.5.0-beta1/test_module.2.5.0-beta1.nupkg"
43+
}
44+
45+
It 'Should select the url when the version is passed as a query parameter' {
46+
$responses = @(
47+
"https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.30",
48+
"https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.3"
49+
)
50+
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($responses, '1.2.3')
51+
$url | Should -BeExactly "https://www.myget.org/api/download?packageId=test_module&packageVersion=1.2.3"
52+
}
53+
54+
It 'Should not select any url when the requested version is not present' {
55+
$url = [Microsoft.PowerShell.PSResourceGet.UtilClasses.TestHooks]::SelectV3PackageContentUrl($versionedResponses, '1.2.4')
56+
$url | Should -BeNullOrEmpty
57+
}
58+
}

0 commit comments

Comments
 (0)