#pragma warning disable SA1512,SA1124 // Single-line comments should not be followed by blank line
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NugetForUnity.Configuration;
using NugetForUnity.Helper;
using NugetForUnity.Models;
using UnityEditor;
using UnityEngine;
#region No ReShaper
// ReSharper disable All
// needed because 'JetBrains.Annotations.NotNull' and 'System.Diagnostics.CodeAnalysis.NotNull' collide if this file is compiled with a never version of Unity / C#
using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
// ReSharper restore All
#endregion
#pragma warning restore SA1512,SA1124 // Single-line comments should not be followed by blank line
namespace NugetForUnity.PackageSource
{
///
/// API client for NuGet API v3.
///
[Serializable]
internal sealed class NugetApiClientV3 : IDisposable
{
[NonSerialized]
[NotNull]
private readonly Uri apiIndexJsonUrl;
[NonSerialized]
[NotNull]
private readonly HttpClient httpClient;
[NonSerialized]
[CanBeNull]
private TaskCompletionSource initializationTaskCompletionSource;
// Example: https://api.nuget.org/v3-flatcontainer/
[CanBeNull]
[SerializeField]
private string packageBaseAddress;
// Example: https://api.nuget.org/v3-flatcontainer/{0}/{1}/{0}.{1}.nupkg
[CanBeNull]
[SerializeField]
private string packageDownloadUrlTemplate;
// Example: https://api.nuget.org/v3/registration5-gz-semver2/
[CanBeNull]
[SerializeField]
private string registrationsBaseUrl;
[CanBeNull]
[SerializeField]
private List searchQueryServices;
///
/// Initializes a new instance of the class.
///
/// The absolute 'index.json' URL of the API.
public NugetApiClientV3([NotNull] string url)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentException($"'{nameof(url)}' cannot be null or whitespace.", nameof(url));
}
apiIndexJsonUrl = new Uri(url);
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate };
if (Application.platform == RuntimePlatform.WindowsEditor)
{
// On Windows, Mono HttpClient does not automatically pick up proxy settings.
handler.Proxy = WebRequest.GetSystemWebProxy();
}
httpClient = new HttpClient(handler);
InitializeFromSessionState();
}
///
/// Gets or sets a optional overwrite for the URL used to download '.nupkg' files (see: ).
///
[CanBeNull]
[field: SerializeField]
public string PackageDownloadUrlTemplateOverwrite { get; set; }
///
public void Dispose()
{
httpClient.Dispose();
}
///
/// Searches for NuGet packages available on the server.
///
///
/// https://github.com/NuGet/docs.microsoft.com-nuget/blob/live/docs/api/search-query-service-resource.md.
///
/// The package source that owns this client.
///
/// The search query. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters.
///
///
/// The number of results to skip. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters.
///
///
/// The number of results to return. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters.
///
///
/// Whether to include pre-release packages. See https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#request-parameters.
///
///
/// Token to cancel the HTTP request.
///
///
/// A list of s that match the search query.
///
public async Task> SearchPackageAsync(
NugetPackageSourceV3 packageSource,
string searchQuery = "",
int skip = -1,
int take = -1,
bool includePreRelease = false,
CancellationToken cancellationToken = default)
{
var successfullyInitialized = await EnsureInitializedAsync(packageSource);
if (!successfullyInitialized || searchQueryServices == null)
{
return new List();
}
if (searchQueryServices.Count == 0)
{
Debug.LogError($"There are no {nameof(searchQueryServices)} specified in the API '{apiIndexJsonUrl}' so we can't search.");
return new List();
}
var queryBuilder = new QueryBuilder();
// so both SemVer 1.0.0 and SemVer 2.0.0 compatible packages are returned
queryBuilder.Add("semVerLevel", "2.0.0");
queryBuilder.Add("q", searchQuery);
if (skip > 0)
{
queryBuilder.Add("skip", skip.ToString(CultureInfo.InvariantCulture));
}
if (take > 0)
{
queryBuilder.Add("take", take.ToString(CultureInfo.InvariantCulture));
}
if (includePreRelease)
{
queryBuilder.Add("prerelease", "true");
}
var query = queryBuilder.ToString();
var queryService = searchQueryServices[0];
var responseString = await GetStringFromServerAsync(packageSource, queryService + query, cancellationToken).ConfigureAwait(false);
var searchResult = JsonUtility.FromJson(responseString);
var results = searchResult.data ?? throw new InvalidOperationException($"missing 'data' property in search response:\n{responseString}");
return SearchResultToNugetPackages(results, packageSource);
}
///
/// Download the .nupkg file and store it inside a file at .
///
/// The package source that owns this client.
/// The package to download its .nupkg from.
/// Path where the downloaded file is placed.
/// Hint for the url used to download the .nupkg file from.
/// The async task.
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "We intentionally use lower case.")]
public async Task DownloadNupkgToFileAsync(
NugetPackageSourceV3 packageSource,
INugetPackageIdentifier package,
string outputFilePath,
string downloadUrlHint)
{
string downloadUrl;
if (!string.IsNullOrEmpty(downloadUrlHint))
{
downloadUrl = downloadUrlHint;
}
else
{
var successfullyInitialized = await EnsureInitializedAsync(packageSource);
if (!successfullyInitialized)
{
return;
}
if (string.IsNullOrEmpty(packageDownloadUrlTemplate))
{
Debug.LogError(
$"There are no {nameof(packageBaseAddress)} specified in the API '{apiIndexJsonUrl}' so we can't download packages.");
return;
}
var version = package.Version.ToLowerInvariant();
var id = package.Id.ToLowerInvariant();
downloadUrl = string.Format(packageDownloadUrlTemplate, id, version);
}
using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl))
{
AddHeadersToRequest(request, packageSource, false);
using (var response = await httpClient.SendAsync(request).ConfigureAwait(false))
{
await EnsureResponseIsSuccessAsync(response).ConfigureAwait(false);
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (var fileStream = File.Create(outputFilePath))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}
}
}
///
/// Fetches a single NuGet package from the package registration. Other than this also fetches a list of
/// all available versions.
///
/// The package source that owns this client.
/// The package identifier to receive including the details.
/// Token to cancel the HTTP request.
/// The package or null if we didn't find it.
public async Task GetPackageWithAllVersionsAsync(
NugetPackageSourceV3 packageSource,
INugetPackageIdentifier package,
CancellationToken cancellationToken = default)
{
var registrationItems = await GetRegistrationPageItemsAsync(packageSource, package, cancellationToken);
if (registrationItems is null)
{
return null;
}
var versions = new List();
RegistrationLeafObject latestVersionItem = null;
NugetPackageVersion latestVersion = null;
foreach (var item in registrationItems)
{
if (item.items is null || item.items.Count == 0)
{
item.items = await GetRegistrationPageLeafItems(packageSource, item, cancellationToken).ConfigureAwait(false);
}
foreach (var leafObject in item.items)
{
var catalogEntry = leafObject.CatalogEntry;
if (catalogEntry.version is null)
{
throw new InvalidOperationException(
$"missing '{nameof(catalogEntry.version)}' property in catalog entry:\n{JsonUtility.ToJson(catalogEntry)}");
}
var version = new NugetPackageVersion(catalogEntry.version);
versions.Add(version);
if (latestVersion != null && version <= latestVersion)
{
continue;
}
latestVersion = version;
latestVersionItem = leafObject;
}
}
versions.Sort((v1, v2) => v2.CompareTo(v1));
return CreatePackageFromRegistrationLeaf(packageSource, latestVersionItem, versions);
}
///
/// Gets a single NuGet package including the package details but not containing all available versions.
///
/// The package source that owns this client.
/// The package identifier to receive including the details.
/// Token to cancel the HTTP request.
/// The package or null if we didn't find it.
[ItemCanBeNull]
public async Task GetPackageWithDetailsAsync(
NugetPackageSourceV3 packageSource,
INugetPackageIdentifier package,
CancellationToken cancellationToken = default)
{
var leafItem = await GetPackageRegistrationLeafAsync(packageSource, package, cancellationToken).ConfigureAwait(false);
if (leafItem is null)
{
return null;
}
return CreatePackageFromRegistrationLeaf(packageSource, leafItem);
}
///
/// Fetches the package details from the server.
///
/// The package source that owns this client.
/// The package to receive the details for.
///
/// Token to cancel the request.
///
///
/// The package details or null if the package is not found.
///
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "API uses lower case.")]
[NotNull]
[ItemNotNull]
public async Task> GetPackageDetailsAsync(
NugetPackageSourceV3 packageSource,
INugetPackageIdentifier package,
CancellationToken cancellationToken = default)
{
var leafItem = await GetPackageRegistrationLeafAsync(packageSource, package, cancellationToken).ConfigureAwait(false);
return leafItem is null ? new List() : ConvertDependencyGroups(leafItem.CatalogEntry);
}
private static NugetPackageV3 CreatePackageFromRegistrationLeaf(
NugetPackageSourceV3 packageSource,
RegistrationLeafObject leafItem,
List allVersions = null)
{
var entry = leafItem.CatalogEntry;
if (entry.id is null)
{
throw new InvalidOperationException($"missing '{nameof(entry.id)}' property in catalog entry:\n{JsonUtility.ToJson(entry)}");
}
if (entry.version is null)
{
throw new InvalidOperationException($"missing '{nameof(entry.version)}' property in catalog entry:\n{JsonUtility.ToJson(entry)}");
}
return new NugetPackageV3(
entry.id,
entry.version,
new List { entry.authors },
entry.description,
0,
entry.licenseUrl,
packageSource,
entry.projectUrl,
entry.summary,
entry.title,
entry.iconUrl,
allVersions ?? new List { new NugetPackageVersion(entry.version) })
{
DownloadUrl = leafItem.packageContent, Dependencies = ConvertDependencyGroups(entry),
};
}
private static List ConvertDependencyGroups(CatalogEntry entry)
{
if (entry.dependencyGroups != null)
{
return entry.dependencyGroups.ConvertAll(
dependencyGroup =>
{
var dependencies = dependencyGroup.dependencies is null ?
new List() :
dependencyGroup.dependencies.ConvertAll(
dependency => (INugetPackageIdentifier)new NugetPackageIdentifier(
dependency.id ??
throw new InvalidOperationException(
$"missing '{nameof(dependency.id)}' inside '{nameof(dependencyGroup.dependencies)}' for dependency group: '{JsonUtility.ToJson(dependencyGroup)}'"),
dependency.range));
return new NugetFrameworkGroup { Dependencies = dependencies, TargetFramework = dependencyGroup.targetFramework };
});
}
if (ConfigurationManager.IsVerboseLoggingEnabled)
{
NugetLogger.LogVerbose(
"missing '{0}.{1}' property for CatalogEntry: '{2}'",
nameof(CatalogEntry),
nameof(CatalogEntry.dependencyGroups),
JsonUtility.ToJson(entry));
}
return new List();
}
private static List SearchResultToNugetPackages(List searchResults, NugetPackageSourceV3 packageSource)
{
var packages = new List(searchResults.Count);
foreach (var item in searchResults)
{
if (item.versions is null)
{
throw new InvalidOperationException(
$"missing '{nameof(item.versions)}' property in search result item:\n{JsonUtility.ToJson(item)}");
}
if (item.id is null)
{
throw new InvalidOperationException($"missing '{nameof(item.id)}' property in search result item:\n{JsonUtility.ToJson(item)}");
}
if (item.version is null)
{
throw new InvalidOperationException(
$"missing '{nameof(item.version)}' property in search result item:\n{JsonUtility.ToJson(item)}");
}
var versions = item.versions.ConvertAll(searchVersion => new NugetPackageVersion(searchVersion.version));
versions.Sort((v1, v2) => v2.CompareTo(v1));
packages.Add(
new NugetPackageV3(
item.id,
item.version,
item.authors ?? new List(),
item.description,
item.totalDownloads,
item.licenseUrl,
packageSource,
item.projectUrl,
item.summary,
item.title,
item.iconUrl,
versions));
}
return packages;
}
private static async Task EnsureResponseIsSuccessAsync(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new HttpRequestException(
$"The request to '{response.RequestMessage.RequestUri}' failed with status code '{response.StatusCode}' and message: {responseString}");
}
[ItemCanBeNull]
private async Task GetPackageRegistrationLeafAsync(
NugetPackageSourceV3 packageSource,
INugetPackageIdentifier package,
CancellationToken cancellationToken = default)
{
var registrationItems = await GetRegistrationPageItemsAsync(packageSource, package, cancellationToken);
if (registrationItems is null)
{
return null;
}
var getLatestVersion = string.IsNullOrEmpty(package.Version);
var pageItem = getLatestVersion ?
registrationItems.OrderByDescending(registrationItem => new NugetPackageVersion(registrationItem.lower)).First() :
registrationItems.Find(
registrationItem =>
new NugetPackageVersion($"[{registrationItem.lower},{registrationItem.upper}]").InRange(package.PackageVersion));
if (pageItem is null)
{
Debug.LogError($"There is no package with id '{package.Id}' and version '{package.Version}' on the registration page.");
return null;
}
if (pageItem.items is null || pageItem.items.Count == 0)
{
pageItem.items = await GetRegistrationPageLeafItems(packageSource, pageItem, cancellationToken).ConfigureAwait(false);
}
var leafItem = getLatestVersion ?
pageItem.items.OrderByDescending(registrationLeaf => new NugetPackageVersion(registrationLeaf.CatalogEntry.version))
.FirstOrDefault() :
pageItem.items.Find(leaf => package.PackageVersion.InRange(new NugetPackageVersion(leaf.CatalogEntry.version)));
if (leafItem is null)
{
Debug.LogError(
$"There is no package with id '{package.Id}' and version '{package.PackageVersion}' in the registration page with the matching version rage: {pageItem.lower} to {pageItem.upper} '{pageItem.atId}'.");
return null;
}
return leafItem;
}
[ItemNotNull]
private async Task> GetRegistrationPageLeafItems(
NugetPackageSourceV3 packageSource,
RegistrationPageObject item,
CancellationToken cancellationToken)
{
// If the items property is not present in the registration page object,
// the URL specified in the @id must be used to fetch metadata about individual package versions.
// The items array is sometimes excluded from the page object as an optimization.
// If the number of versions of a single package ID is very large, then the registration index document will be massive and wasteful
// to process for a client that only cares about a specific version or small range of versions.
var itemAtId = item.atId ?? throw new InvalidOperationException($"missing '@id' for item inside item:\n{JsonUtility.ToJson(item)}");
var registrationPageString = await GetStringFromServerAsync(packageSource, itemAtId, cancellationToken).ConfigureAwait(false);
var registrationPage = JsonUtility.FromJson(registrationPageString);
return registrationPage.items ??
throw new InvalidOperationException(
$"missing 'items' property inside page request for URL: {itemAtId}, response:\n{registrationPageString}");
}
private async Task> GetRegistrationPageItemsAsync(
NugetPackageSourceV3 packageSource,
INugetPackageIdentifier package,
CancellationToken cancellationToken)
{
var successfullyInitialized = await EnsureInitializedAsync(packageSource);
if (!successfullyInitialized)
{
return null;
}
if (string.IsNullOrEmpty(registrationsBaseUrl))
{
Debug.LogError(
$"There are no {nameof(registrationsBaseUrl)} specified in the API '{apiIndexJsonUrl}' so we can't receive package details.");
return null;
}
string responseString;
try
{
responseString = await GetStringFromServerAsync(
packageSource,
$"{registrationsBaseUrl}{package.Id.ToLowerInvariant()}/index.json",
cancellationToken)
.ConfigureAwait(false);
}
catch (HttpRequestException exception)
{
Debug.LogWarning($"Failed to get '{package.Id}' from the package source '{apiIndexJsonUrl}'. Error: {exception}");
return null;
}
var registrationResponse = JsonUtility.FromJson(responseString.Replace(@"""@id"":", @"""atId"":"));
// without a version specified, the latest version is returned
var registrationItems = registrationResponse.items ??
throw new InvalidOperationException(
$"missing 'items' property inside registration request for package: {package.Id}, response:\n{responseString}");
return registrationItems;
}
private void InitializeFromSessionState()
{
var sessionState = SessionState.GetString(GetSessionStateKey(), string.Empty);
if (string.IsNullOrEmpty(sessionState))
{
return;
}
JsonUtility.FromJsonOverwrite(sessionState, this);
}
private void SaveToSessionState()
{
UnityMainThreadDispatcher.Dispatch(() => SessionState.SetString(GetSessionStateKey(), JsonUtility.ToJson(this)));
}
private string GetSessionStateKey()
{
return $"{nameof(NugetApiClientV3)}:{apiIndexJsonUrl}";
}
private async Task EnsureInitializedAsync(NugetPackageSourceV3 packageSource)
{
if (searchQueryServices != null)
{
return true;
}
try
{
var successful = await AwaitableInitializeApiAddressesAsync(packageSource);
return successful;
}
catch (Exception exception)
{
Debug.LogError($"Initialization of api client for '{apiIndexJsonUrl}' failed so we can't use it. error (cached): {exception}.");
return false;
}
}
private Task AwaitableInitializeApiAddressesAsync(NugetPackageSourceV3 packageSource)
{
if (initializationTaskCompletionSource != null)
{
return initializationTaskCompletionSource.Task;
}
lock (httpClient)
{
if (initializationTaskCompletionSource != null)
{
return initializationTaskCompletionSource.Task;
}
initializationTaskCompletionSource = new TaskCompletionSource();
}
_ = InitializeApiAddressesAsync(packageSource);
return initializationTaskCompletionSource.Task;
}
private async Task InitializeApiAddressesAsync(NugetPackageSourceV3 packageSource)
{
Debug.Assert(initializationTaskCompletionSource != null, "initializationTaskCompletionSource != null");
try
{
var responseString = await GetStringFromServerAsync(packageSource, apiIndexJsonUrl.AbsoluteUri, CancellationToken.None)
.ConfigureAwait(false);
var resourceList = JsonUtility.FromJson(
responseString.Replace(@"""@id"":", @"""atId"":").Replace(@"""@type"":", @"""atType"":"));
var foundSearchQueryServices = new List();
var resources = resourceList.resources ??
throw new InvalidOperationException(
$"missing '{nameof(resourceList.resources)}' property inside index response:\n{responseString}");
// we only support v3 so if v4 is released we skip it.
var maxSupportedApiVersion = new NugetPackageVersion("4.0.0");
NugetPackageVersion highestPackageBaseAddressApiVersion = null;
NugetPackageVersion highestRegistrationsBaseUrlApiVersion = null;
NugetPackageVersion highestSearchQueryServiceApiVersion = null;
foreach (var resource in resources)
{
var resourceAtId = resource.atId ??
throw new InvalidOperationException($"missing '@id' property inside resource of type '{resource.atType}'");
var resourceAtType = resource.atType ??
throw new InvalidOperationException($"missing '@type' property inside resource with id '{resource.atId}'");
var resourceTypeParts = resourceAtType.Split('/');
NugetPackageVersion resourceTypeVersion = null;
// need to skip if version is no number like in: 'RegistrationsBaseUrl/Versioned'
if (resourceTypeParts.Length > 1 && !string.IsNullOrEmpty(resourceTypeParts[1]) && char.IsDigit(resourceTypeParts[1][0]))
{
resourceTypeVersion = new NugetPackageVersion(resourceTypeParts[1]);
}
switch (resourceTypeParts[0])
{
case "SearchQueryService":
if (highestSearchQueryServiceApiVersion == null ||
(resourceTypeVersion > highestSearchQueryServiceApiVersion && resourceTypeVersion < maxSupportedApiVersion))
{
highestSearchQueryServiceApiVersion = resourceTypeVersion;
var comment = resource.comment ?? string.Empty;
if (comment.IndexOf("(primary)", StringComparison.OrdinalIgnoreCase) >= 0)
{
foundSearchQueryServices.Insert(0, resourceAtId.Trim('/'));
}
else
{
foundSearchQueryServices.Add(resourceAtId.Trim('/'));
}
}
break;
case "PackageBaseAddress":
if (highestPackageBaseAddressApiVersion == null ||
(resourceTypeVersion > highestPackageBaseAddressApiVersion && resourceTypeVersion < maxSupportedApiVersion))
{
highestPackageBaseAddressApiVersion = resourceTypeVersion;
packageBaseAddress = resourceAtId.Trim('/') + '/';
}
break;
case "RegistrationsBaseUrl":
if (highestRegistrationsBaseUrlApiVersion == null ||
(resourceTypeVersion > highestRegistrationsBaseUrlApiVersion && resourceTypeVersion < maxSupportedApiVersion))
{
highestRegistrationsBaseUrlApiVersion = resourceTypeVersion;
registrationsBaseUrl = resourceAtId.Trim('/') + '/';
}
break;
}
}
if (!string.IsNullOrEmpty(PackageDownloadUrlTemplateOverwrite))
{
packageDownloadUrlTemplate = PackageDownloadUrlTemplateOverwrite;
}
else if (string.IsNullOrEmpty(packageBaseAddress))
{
UnityMainThreadDispatcher.Dispatch(
() =>
{
var displayDialog = EditorUtility.DisplayDialog(
"Missing 'PackageBaseAddress' endpoint",
$"The used NuGet V3 API '{apiIndexJsonUrl}' doesn't support the 'PackageBaseAddress' endpoint. You need to provide the url manually by specifying '{NugetConfigFile.PackageDownloadUrlTemplateOverwriteAttributeName}' on the package source. If this is a NuGet source provided by Artifactory you can click on 'Configure for Artifactory' to configure it.",
"Configure for Artifactory",
"Cancel");
if (displayDialog)
{
packageDownloadUrlTemplate = $"{registrationsBaseUrl}Download/{{0}}/{{1}}";
PackageDownloadUrlTemplateOverwrite = packageDownloadUrlTemplate;
// Artifactory somehow can't handle search queries containing multiple packageId's.
packageSource.UpdateSearchBatchSize = 1;
SaveToSessionState();
ConfigurationManager.NugetConfigFile.Save(ConfigurationManager.NugetConfigFilePath);
}
else
{
Debug.LogErrorFormat(
"The NuGet package source at '{0}' has no PackageBaseAddress resource defined, please specify it manually by adding the '{1}' attribute on the package-source configuration inside the '{2}' file.",
apiIndexJsonUrl,
NugetConfigFile.PackageDownloadUrlTemplateOverwriteAttributeName,
NugetConfigFile.FileName);
}
});
}
else
{
packageDownloadUrlTemplate = $"{packageBaseAddress}{{0}}/{{1}}/{{0}}.{{1}}.nupkg";
PackageDownloadUrlTemplateOverwrite = null;
}
searchQueryServices = foundSearchQueryServices;
SaveToSessionState();
var successful = searchQueryServices.Count > 0;
initializationTaskCompletionSource.SetResult(successful);
}
catch (Exception exception)
{
Debug.LogErrorFormat("Failed to initialize the NuGet package source '{0}'. Error: {1}", apiIndexJsonUrl, exception);
initializationTaskCompletionSource.SetException(exception);
}
}
private async Task GetStringFromServerAsync(NugetPackageSourceV3 packageSource, string url, CancellationToken cancellationToken)
{
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
AddHeadersToRequest(request, packageSource, true);
using (var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false))
{
await EnsureResponseIsSuccessAsync(response).ConfigureAwait(false);
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false) ?? string.Empty;
return responseString;
}
}
}
private void AddHeadersToRequest(HttpRequestMessage request, NugetPackageSourceV3 packageSource, bool expectJsonResponse)
{
if (expectJsonResponse)
{
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
}
request.Headers.Add("User-Agent", "NuGetForUnity");
var password = packageSource.ExpandedPassword;
var userName = packageSource.UserName;
if (string.IsNullOrEmpty(password))
{
var creds = CredentialProviderHelper.GetCredentialFromProvider(apiIndexJsonUrl);
if (creds.HasValue)
{
userName = creds.Value.UserName;
password = creds.Value.Password;
}
}
if (!string.IsNullOrEmpty(password))
{
request.Headers.Authorization = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}")));
}
}
private sealed class QueryBuilder
{
private readonly StringBuilder builder = new StringBuilder();
public void Add(string parameterName, string parameterValue)
{
if (string.IsNullOrEmpty(parameterValue))
{
return;
}
builder.Append(builder.Length == 0 ? '?' : '&');
builder.Append(parameterName);
builder.Append('=');
builder.Append(Uri.EscapeDataString(parameterValue));
}
public override string ToString()
{
return builder.ToString();
}
}
// ReSharper disable InconsistentNaming
// ReSharper disable UnassignedField.Local
// ReSharper disable UnusedMember.Local
// ReSharper disable NotAccessedField.Local
#pragma warning disable CS0649 // Field is assigned on serialize
#pragma warning disable CA1051 // Do not declare visible instance fields
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
#pragma warning disable SA1401 // Fields should be private
#pragma warning disable S1144 // Unused private types or members should be removed
[Serializable]
private sealed class IndexResponse
{
[CanBeNull]
public List resources;
[CanBeNull]
public string version;
}
[Serializable]
private sealed class Resource
{
[CanBeNull]
public string atId;
[CanBeNull]
public string atType;
[CanBeNull]
public string clientVersion;
[CanBeNull]
public string comment;
}
[Serializable]
private sealed class RegistrationResponse
{
///
/// The number of registration pages in the index.
///
public int count;
///
/// The array of registration pages.
///
[CanBeNull]
public List items;
}
[Serializable]
private sealed class RegistrationPageObject
{
///
/// The URL to the registration page.
/// If the items property is not present in the registration page object, the URL specified in the @id must be used to fetch metadata about
/// individual package versions.
///
[CanBeNull]
public string atId;
///
/// The number of registration leaves in the page.
///
public int count;
///
/// The array of registration leaves and their associate metadata.
///
[CanBeNull]
public List items;
///
/// The lowest SemVer 2.0.0 version in the page (inclusive).
///
[CanBeNull]
public string lower;
///
/// The highest SemVer 2.0.0 version in the page (inclusive).
///
[CanBeNull]
public string upper;
}
[Serializable]
private sealed class RegistrationLeafObject
{
///
/// The URL to the registration leaf.
///
[CanBeNull]
public string atId;
///
/// The catalog entry containing the package metadata.
///
[CanBeNull]
public CatalogEntry catalogEntry;
///
/// The URL to the package content (.nupkg).
///
[CanBeNull]
public string packageContent;
public CatalogEntry CatalogEntry =>
catalogEntry ?? throw new InvalidOperationException($"missing '{nameof(catalogEntry)}' property in registration leaf '{atId}'.");
}
// Removed property because it has a 'string or array of strings' representation and therefor causes parse errors when using the CLI: 'tags'
[Serializable]
private sealed class CatalogEntry
{
///
/// The URL to the document used to produce this object.
///
[CanBeNull]
public string atId;
[CanBeNull]
public string authors;
///
/// The dependencies of the package, grouped by target framework.
///
[CanBeNull]
public List dependencyGroups;
///
/// The deprecation associated with the package.
///
[CanBeNull]
public Deprecation deprecation;
[CanBeNull]
public string description;
[CanBeNull]
public string iconUrl;
///
/// The ID of the package.
///
[CanBeNull]
public string id;
[CanBeNull]
public string language;
[CanBeNull]
public string licenseExpression;
[CanBeNull]
public string licenseUrl;
///
/// Should be considered as listed if absent.
///
public bool listed = true;
[CanBeNull]
public string minClientVersion;
[CanBeNull]
public string projectUrl;
///
/// A string containing a ISO 8601 time-stamp of when the package was published.
///
[CanBeNull]
public string published;
///
/// A URL for the rendered (HTML web page) view of the package README.
///
[CanBeNull]
public string readmeUrl;
public bool requireLicenseAcceptance;
[CanBeNull]
public string summary;
[CanBeNull]
public string title;
///
/// The full version string after normalization.
///
[CanBeNull]
public string version;
///
/// The security vulnerabilities of the package.
///
[CanBeNull]
public List vulnerabilities;
}
[Serializable]
private sealed class DependencyGroup
{
[CanBeNull]
public List dependencies;
///
/// The target framework that these dependencies are applicable to.
///
public string targetFramework = string.Empty;
}
[Serializable]
private sealed class Dependency
{
///
/// The ID of the package dependency.
///
[CanBeNull]
public string id;
///
/// The allowed version range of the dependency.
///
[CanBeNull]
public string range;
///
/// The URL to the registration index for this dependency.
///
[CanBeNull]
public string registration;
}
[Serializable]
private sealed class Deprecation
{
///
/// The additional details about this deprecation.
///
[CanBeNull]
public string message;
///
/// The reasons why the package was deprecated.
///
[CanBeNull]
public List reasons;
}
[Serializable]
private sealed class Vulnerability
{
///
/// Location of security advisory for the package.
///
[CanBeNull]
public string advisoryUrl;
///
/// Severity of advisory: "0" = Low, "1" = Moderate, "2" = High, "3" = Critical.
///
[CanBeNull]
public string severity;
}
[Serializable]
private sealed class SearchResult
{
///
/// The search results matched by the request.
///
[CanBeNull]
public List data;
///
/// The total number of matches, disregarding skip and take.
///
public int totalHits;
}
[Serializable]
private sealed class SearchResultItem
{
[CanBeNull]
public List authors;
[CanBeNull]
public string description;
[CanBeNull]
public string iconUrl;
///
/// The ID of the matched package.
///
[CanBeNull]
public string id;
[CanBeNull]
public string licenseUrl;
[CanBeNull]
public List owners;
[CanBeNull]
public string projectUrl;
///
/// The absolute URL to the associated registration index.
///
[CanBeNull]
public string registration;
[CanBeNull]
public string summary;
[CanBeNull]
public List tags;
[CanBeNull]
public string title;
///
/// This value can be inferred by the sum of downloads in the versions array.
///
public long totalDownloads;
///
/// A JSON boolean indicating whether the package is verified.
///
public bool verified;
///
/// The full SemVer 2.0.0 version string of the package (could contain build metadata).
///
[CanBeNull]
public string version;
///
/// All of the versions of the package considering the prerelease parameter.
///
[CanBeNull]
public List versions;
}
[Serializable]
private sealed class SearchResultVersion
{
///
/// The number of downloads for this specific package version.
///
public long downloads;
///
/// The full SemVer 2.0.0 version string of the package (could contain build metadata).
///
[CanBeNull]
public string version;
}
// ReSharper restore InconsistentNaming
// ReSharper restore UnassignedField.Local
// ReSharper restore UnusedMember.Local
// ReSharper restore NotAccessedField.Local
#pragma warning restore CS0649 // Field is assigned on serialize
#pragma warning restore CA1051 // Do not declare visible instance fields
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
#pragma warning restore SA1401 // Fields should be private
#pragma warning restore S1144 // Unused private types or members should be removed
}
}