Skip to content

Commit 2abcb8e

Browse files
authored
Fix V3 packageContent URL selection to compare parsed versions (#2019)
1 parent 519eb7a commit 2abcb8e

5 files changed

Lines changed: 268 additions & 141 deletions

File tree

‎src/code/InstallHelper.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ private ConcurrentDictionary<string, Hashtable> InstallParentAndDependencyPackag
913913
Parallel.ForEach(parentAndDeps, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, depPkg =>
914914
{
915915
var depPkgName = depPkg.Name;
916-
var depPkgVersion = depPkg.Version.ToString();
916+
var depPkgVersion = Utils.GetFullVersionString(depPkg.Version.ToString(), depPkg.Prerelease);
917917

918918
verboseMsgs.Enqueue($"Installing package '{depPkgName}' version '{depPkgVersion}'");
919919
//Stream responseStream = currentServer.InstallPackage(depPkgName, depPkgVersion, true, out ErrorRecord installNameErrRecord);

‎src/code/PSResourceInfo.cs‎

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,51 +2162,4 @@ private static Dependency[] GetDependenciesForPsd1(Hashtable[] requiredModules)
21622162
}
21632163

21642164
#endregion
2165-
2166-
#region Test Hooks
2167-
2168-
public static class TestHooks
2169-
{
2170-
public static PSObject ReadPSGetResourceInfo(string filePath)
2171-
{
2172-
if (PSResourceInfo.TryRead(filePath, out PSResourceInfo psGetInfo, out string errorMsg))
2173-
{
2174-
return PSObject.AsPSObject(psGetInfo);
2175-
}
2176-
2177-
throw new PSInvalidOperationException(errorMsg);
2178-
}
2179-
2180-
public static PSObject ConvertFromJson(string json, PSRepositoryInfo repository)
2181-
{
2182-
using (JsonDocument pkgJson = JsonDocument.Parse(json))
2183-
{
2184-
if (PSResourceInfo.TryConvertFromJson(pkgJson, out PSResourceInfo psGetInfo, repository, out string errorMsg))
2185-
{
2186-
return PSObject.AsPSObject(psGetInfo);
2187-
}
2188-
2189-
throw new PSInvalidOperationException(errorMsg);
2190-
}
2191-
}
2192-
2193-
public static void WritePSGetResourceInfo(
2194-
string filePath,
2195-
PSObject psObjectGetInfo)
2196-
{
2197-
if (psObjectGetInfo.BaseObject is PSResourceInfo psGetInfo)
2198-
{
2199-
if (!psGetInfo.TryWrite(filePath, out string errorMsg))
2200-
{
2201-
throw new PSInvalidOperationException(errorMsg);
2202-
}
2203-
2204-
return;
2205-
}
2206-
2207-
throw new PSArgumentException("psObjectGetInfo argument is not a PSGetResourceInfo type.");
2208-
}
2209-
}
2210-
2211-
#endregion
22122165
}

‎src/code/TestHooks.cs‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.PowerShell.PSResourceGet.Cmdlets;
5+
using NuGet.Versioning;
6+
using System.Management.Automation;
7+
using System.Text.Json;
8+
9+
namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
10+
{
11+
/// <summary>
12+
/// Entry points that expose internal functionality to the Pester tests.
13+
/// </summary>
14+
public static class TestHooks
15+
{
16+
public static PSObject ReadPSGetResourceInfo(string filePath)
17+
{
18+
if (PSResourceInfo.TryRead(filePath, out PSResourceInfo psGetInfo, out string errorMsg))
19+
{
20+
return PSObject.AsPSObject(psGetInfo);
21+
}
22+
23+
throw new PSInvalidOperationException(errorMsg);
24+
}
25+
26+
public static PSObject ConvertFromJson(string json, PSRepositoryInfo repository)
27+
{
28+
using (JsonDocument pkgJson = JsonDocument.Parse(json))
29+
{
30+
if (PSResourceInfo.TryConvertFromJson(pkgJson, out PSResourceInfo psGetInfo, repository, out string errorMsg))
31+
{
32+
return PSObject.AsPSObject(psGetInfo);
33+
}
34+
35+
throw new PSInvalidOperationException(errorMsg);
36+
}
37+
}
38+
39+
public static void WritePSGetResourceInfo(
40+
string filePath,
41+
PSObject psObjectGetInfo)
42+
{
43+
if (psObjectGetInfo.BaseObject is PSResourceInfo psGetInfo)
44+
{
45+
if (!psGetInfo.TryWrite(filePath, out string errorMsg))
46+
{
47+
throw new PSInvalidOperationException(errorMsg);
48+
}
49+
50+
return;
51+
}
52+
53+
throw new PSArgumentException("psObjectGetInfo argument is not a PSGetResourceInfo type.");
54+
}
55+
56+
/// <summary>
57+
/// Selects the "packageContent" URL from V3 registration entries for the given version, or for the latest version if no version is given.
58+
/// </summary>
59+
public static string SelectV3PackageContentUrl(
60+
string[] registrationEntries,
61+
string version)
62+
{
63+
NuGetVersion requiredVersion = null;
64+
if (!string.IsNullOrEmpty(version) && !NuGetVersion.TryParse(version, out requiredVersion))
65+
{
66+
throw new PSArgumentException($"Version '{version}' is not a valid NuGet version.");
67+
}
68+
69+
return V3ServerAPICalls.GetPackageContentUrl(registrationEntries, requiredVersion);
70+
}
71+
}
72+
}

‎src/code/V3ServerAPICalls.cs‎

Lines changed: 94 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -883,11 +883,6 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error
883883
{
884884
debugMsgs.Enqueue("In V3ServerAPICalls::InstallHelper()");
885885
Stream pkgStream = null;
886-
bool getLatestVersion = true;
887-
if (version != null)
888-
{
889-
getLatestVersion = false;
890-
}
891886

892887
string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, packageContentProperty, isSearch: false, out errRecord, errorMsgs, debugMsgs, verboseMsgs);
893888
if (errRecord != null)
@@ -906,28 +901,10 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error
906901
return null;
907902
}
908903

909-
string pkgContentUrl = String.Empty;
910-
if (getLatestVersion)
911-
{
912-
pkgContentUrl = versionedResponses[0];
913-
}
914-
else
915-
{
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-
}
927-
}
928-
904+
string pkgContentUrl = GetPackageContentUrl(versionedResponses, version);
929905
if (String.IsNullOrEmpty(pkgContentUrl))
930906
{
907+
debugMsgs.Enqueue($"No registration entry for package '{packageName}' had a 'catalogEntry' version matching '{version}' and a 'packageContent' URL.");
931908
errRecord = new ErrorRecord(
932909
new Exception($"Package with name '{packageName}' and version '{version}' could not be found in repository '{Repository.Name}'"),
933910
"InstallFailure",
@@ -966,11 +943,6 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
966943
{
967944
debugMsgs.Enqueue("In V3ServerAPICalls::InstallHelperAsync()");
968945
Stream pkgStream = null;
969-
bool getLatestVersion = true;
970-
if (version != null)
971-
{
972-
getLatestVersion = false;
973-
}
974946

975947
string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, packageContentProperty, isSearch: false, out ErrorRecord errRecord, errorMsgs, debugMsgs, verboseMsgs);
976948
if (errRecord != null)
@@ -990,28 +962,10 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
990962
return null;
991963
}
992964

993-
string pkgContentUrl = String.Empty;
994-
if (getLatestVersion)
995-
{
996-
pkgContentUrl = versionedResponses[0];
997-
}
998-
else
999-
{
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-
}
1011-
}
1012-
965+
string pkgContentUrl = GetPackageContentUrl(versionedResponses, version);
1013966
if (String.IsNullOrEmpty(pkgContentUrl))
1014967
{
968+
debugMsgs.Enqueue($"No registration entry for package '{packageName}' had a 'catalogEntry' version matching '{version}' and a 'packageContent' URL.");
1015969
errorMsgs.Enqueue(new ErrorRecord(
1016970
new Exception($"Package with name '{packageName}' and version '{version}' could not be found in repository '{Repository.Name}'"),
1017971
"InstallFailure",
@@ -1039,6 +993,87 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
1039993
return pkgStream;
1040994
}
1041995

996+
/// <summary>
997+
/// Selects the "packageContent" URL (i.e the .nupkg download URL) of the registration entry for the required version,
998+
/// or of the latest version if the required version is null.
999+
/// The version is taken from the entry's "catalogEntry" > "version" property and compared as a NuGetVersion,
1000+
/// rather than being inferred from the URL text (a substring search for '1.2.3' would match the URL for '1.2.30').
1001+
/// </summary>
1002+
internal static string GetPackageContentUrl(string[] registrationEntries, NuGetVersion requiredVersion)
1003+
{
1004+
string pkgContentUrl = String.Empty;
1005+
if (registrationEntries == null)
1006+
{
1007+
return pkgContentUrl;
1008+
}
1009+
1010+
NuGetVersion latestVersion = null;
1011+
foreach (string registrationEntry in registrationEntries)
1012+
{
1013+
if (!TryGetPackageContentEntry(registrationEntry, out NuGetVersion entryVersion, out string entryPkgContentUrl))
1014+
{
1015+
continue;
1016+
}
1017+
1018+
if (requiredVersion != null)
1019+
{
1020+
if (entryVersion == requiredVersion)
1021+
{
1022+
return entryPkgContentUrl;
1023+
}
1024+
}
1025+
else if (latestVersion == null || entryVersion > latestVersion)
1026+
{
1027+
latestVersion = entryVersion;
1028+
pkgContentUrl = entryPkgContentUrl;
1029+
}
1030+
}
1031+
1032+
return pkgContentUrl;
1033+
}
1034+
1035+
/// <summary>
1036+
/// Gets the version and "packageContent" URL from a registration entry, which looks like:
1037+
/// { "catalogEntry": { "version": "1.0.0", ... }, "packageContent": "https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg", ... }
1038+
/// </summary>
1039+
private static bool TryGetPackageContentEntry(string registrationEntry, out NuGetVersion version, out string pkgContentUrl)
1040+
{
1041+
version = null;
1042+
pkgContentUrl = String.Empty;
1043+
if (String.IsNullOrWhiteSpace(registrationEntry))
1044+
{
1045+
return false;
1046+
}
1047+
1048+
try
1049+
{
1050+
using (JsonDocument registrationEntryJson = JsonDocument.Parse(registrationEntry))
1051+
{
1052+
JsonElement rootDom = registrationEntryJson.RootElement;
1053+
if (rootDom.ValueKind != JsonValueKind.Object ||
1054+
!rootDom.TryGetProperty(packageContentProperty, out JsonElement pkgContentElement) ||
1055+
pkgContentElement.ValueKind != JsonValueKind.String ||
1056+
!rootDom.TryGetProperty(catalogEntryProperty, out JsonElement catalogEntryElement) ||
1057+
catalogEntryElement.ValueKind != JsonValueKind.Object ||
1058+
!catalogEntryElement.TryGetProperty(versionName, out JsonElement versionElement) ||
1059+
!NuGetVersion.TryParse(versionElement.ToString(), out version))
1060+
{
1061+
version = null;
1062+
return false;
1063+
}
1064+
1065+
pkgContentUrl = pkgContentElement.GetString();
1066+
}
1067+
}
1068+
catch (JsonException)
1069+
{
1070+
version = null;
1071+
return false;
1072+
}
1073+
1074+
return !String.IsNullOrWhiteSpace(pkgContentUrl);
1075+
}
1076+
10421077
/// <summary>
10431078
/// Gets the versioned package entries from the RegistrationsBaseUrl resource
10441079
/// i.e when the package Name being searched for does not contain wildcard
@@ -1451,8 +1486,9 @@ private string[] GetMetadataElementsFromResponse(string response, string propert
14511486

14521487
if (metadataElement.ValueKind == JsonValueKind.String)
14531488
{
1454-
// This is when property is "packageContent"
1455-
versionedPkgResponses.Add(metadataElement.ToString());
1489+
// This is when property is "packageContent".
1490+
// The whole entry is returned so the version can be read from its "catalogEntry" rather than inferred from the URL.
1491+
versionedPkgResponses.Add(item.ToString());
14561492
}
14571493
else if(metadataElement.ValueKind == JsonValueKind.Object)
14581494
{
@@ -1494,7 +1530,8 @@ private string[] GetMetadataElementsFromResponse(string response, string propert
14941530
/// This contains an inner items element (containing the package metadata) and the packageContent element (containing URI through which the .nupkg can be downloaded)
14951531
/// <param name="property"> This can be the "catalogEntry" or "packageContent" property.
14961532
/// The "catalogEntry" property is used for search, and the value is package metadata.
1497-
/// The "packageContent" property is used for download, and the value is a URI for the .nupkg file.
1533+
/// The "packageContent" property is used for download, and the value is the whole registration entry,
1534+
/// containing both the "catalogEntry" (with the package version) and the "packageContent" URI for the .nupkg file.
14981535
/// </param>
14991536
/// <summary>
15001537
private string[] GetVersionedResponsesFromRegistrationsResource(string registrationsBaseUrl, string packageName, string property, bool isSearch, out ErrorRecord errRecord, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
@@ -1526,25 +1563,16 @@ private string[] GetVersionedResponsesFromRegistrationsResource(string registrat
15261563
}
15271564

15281565
// Reverse array of versioned responses, if needed, so that version entries are in descending order.
1529-
if (String.IsNullOrEmpty(upperVersion))
1566+
// Install selects its entry by comparing versions, so it does not depend on the order.
1567+
if (String.IsNullOrEmpty(upperVersion) || !isSearch)
15301568
{
15311569
// add write Debug and use these results
15321570
return versionedResponseArr;
15331571
}
15341572

1535-
if (isSearch)
1573+
if (!IsLatestVersionFirstForSearch(versionedResponseArr, out errRecord, errorMsgs, debugMsgs, verboseMsgs))
15361574
{
1537-
if (!IsLatestVersionFirstForSearch(versionedResponseArr, out errRecord, errorMsgs, debugMsgs, verboseMsgs))
1538-
{
1539-
Array.Reverse(versionedResponseArr);
1540-
}
1541-
}
1542-
else
1543-
{
1544-
if (!IsLatestVersionFirstForInstall(versionedResponseArr, upperVersion, out errRecord, errorMsgs, debugMsgs, verboseMsgs))
1545-
{
1546-
Array.Reverse(versionedResponseArr);
1547-
}
1575+
Array.Reverse(versionedResponseArr);
15481576
}
15491577

15501578
return versionedResponseArr;
@@ -1649,33 +1677,6 @@ private bool IsLatestVersionFirstForSearch(string[] versionedResponses, out Erro
16491677
return latestVersionFirst;
16501678
}
16511679

1652-
/// <summary>
1653-
/// Returns true if the nupkg URI entries for each package version are arranged in descending order with respect to the package's version.
1654-
/// ADO feeds usually return version entries in descending order, but Nuget.org repository returns them in ascending order.
1655-
/// Entries do not reflect prerelease preference so all versions (including prerelease) are being considered here, so upper version (including prerelease) can be used for comparison.
1656-
/// </summary>
1657-
private bool IsLatestVersionFirstForInstall(string[] versionedResponses, string upperVersion, out ErrorRecord errRecord, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
1658-
{
1659-
debugMsgs.Enqueue("In V3ServerAPICalls::IsLatestVersionFirstForInstall()");
1660-
errRecord = null;
1661-
bool latestVersionFirst = true;
1662-
1663-
// We don't need to perform this check if no responses, or single response
1664-
if (versionedResponses.Length < 2)
1665-
{
1666-
return latestVersionFirst;
1667-
}
1668-
1669-
string firstResponse = versionedResponses[0];
1670-
// for Install, response will be a URI value for the package .nupkg, not JSON
1671-
if (!firstResponse.Contains(upperVersion))
1672-
{
1673-
latestVersionFirst = false;
1674-
}
1675-
1676-
return latestVersionFirst;
1677-
}
1678-
16791680
/// <summary>
16801681
/// Helper method that determines if specified tags are present in package's tags.
16811682
/// </summary>

0 commit comments

Comments
 (0)