@@ -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
0 commit comments