|
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Linq;
using System.Text.Json;
using Newtonsoft.Json.Linq;
using NuGet.Configuration;
using NuGet.Packaging;
using NuGet.Protocol.Core.Types;
using NuGet.Protocol.Events;
using NuGet.Protocol.Model;
using NuGet.Protocol.Utility;
using NuGet.Versioning;
namespace NuGet.Protocol
{
/// <summary>
/// Stores/caches a service index json file.
/// </summary>
public class ServiceIndexResourceV3 : INuGetResource
{
private string? _json;
private readonly ServiceIndexModel? _model;
private readonly IDictionary<string, List<ServiceIndexEntry>> _index;
private readonly DateTime _requestTime;
private static readonly IReadOnlyList<ServiceIndexEntry> _emptyEntries = new List<ServiceIndexEntry>();
private static readonly SemanticVersion _defaultVersion = new SemanticVersion(0, 0, 0);
#if NET5_0_OR_GREATER
[RequiresUnreferencedCode("Uses Newtonsoft.Json reflection-based deserialization.")]
[RequiresDynamicCode("Uses Newtonsoft.Json reflection-based deserialization.")]
#endif
internal ServiceIndexResourceV3(JObject index, DateTime requestTime, PackageSource? packageSource)
{
_ = index ?? throw new ArgumentNullException(nameof(index));
_json = index.ToString();
_index = MakeLookup(index, packageSource);
_requestTime = requestTime;
}
#if NET5_0_OR_GREATER
[RequiresUnreferencedCode("Uses Newtonsoft.Json reflection-based deserialization.")]
[RequiresDynamicCode("Uses Newtonsoft.Json reflection-based deserialization.")]
#endif
public ServiceIndexResourceV3(JObject index, DateTime requestTime) : this(index, requestTime, packageSource: null) { }
internal ServiceIndexResourceV3(ServiceIndexModel model, DateTime requestTime, PackageSource? packageSource)
{
_ = model ?? throw new ArgumentNullException(nameof(model));
_model = model;
_index = MakeLookup(model, packageSource);
_requestTime = requestTime;
}
/// <summary>
/// Time the index was requested
/// </summary>
public virtual DateTime RequestTime
{
get { return _requestTime; }
}
/// <summary>
/// All service index entries.
/// </summary>
public virtual IReadOnlyList<ServiceIndexEntry> Entries
{
get
{
return _index.SelectMany(e => e.Value).ToList();
}
}
public virtual string Json
{
get { return _json ??= JsonSerializer.Serialize(_model!, JsonContext.Default.ServiceIndexModel); }
}
/// <summary>
/// Get the list of service entries that best match the current clientVersion and type.
/// </summary>
public virtual IReadOnlyList<ServiceIndexEntry> GetServiceEntries(params string[] orderedTypes)
{
var clientVersion = MinClientVersionUtility.GetNuGetClientVersion();
return GetServiceEntries(clientVersion, orderedTypes);
}
/// <summary>
/// Get the list of service entries that best match the clientVersion and type.
/// </summary>
public virtual IReadOnlyList<ServiceIndexEntry> GetServiceEntries(NuGetVersion clientVersion, params string[] orderedTypes)
{
if (clientVersion == null)
{
throw new ArgumentNullException(nameof(clientVersion));
}
foreach (var type in orderedTypes)
{
List<ServiceIndexEntry>? entries;
if (_index.TryGetValue(type, out entries))
{
var compatible = GetBestVersionMatchForType(clientVersion, entries);
if (compatible.Count > 0)
{
return compatible;
}
}
}
return _emptyEntries;
}
private IReadOnlyList<ServiceIndexEntry> GetBestVersionMatchForType(NuGetVersion clientVersion, List<ServiceIndexEntry> entries)
{
var bestMatch = entries.FirstOrDefault(e => e.ClientVersion <= clientVersion);
if (bestMatch == null)
{
// No compatible version
return _emptyEntries;
}
else
{
// Find all entries with the same version.
return entries.Where(e => e.ClientVersion == bestMatch.ClientVersion).ToList();
}
}
/// <summary>
/// Get the best match service URI.
/// </summary>
public virtual Uri? GetServiceEntryUri(params string[] orderedTypes)
{
var clientVersion = MinClientVersionUtility.GetNuGetClientVersion();
return GetServiceEntryUris(clientVersion, orderedTypes).FirstOrDefault();
}
/// <summary>
/// Get the list of service URIs that best match the current clientVersion and type.
/// </summary>
public virtual IReadOnlyList<Uri> GetServiceEntryUris(params string[] orderedTypes)
{
var clientVersion = MinClientVersionUtility.GetNuGetClientVersion();
return GetServiceEntryUris(clientVersion, orderedTypes);
}
/// <summary>
/// Get the list of service URIs that best match the clientVersion and type.
/// </summary>
public virtual IReadOnlyList<Uri> GetServiceEntryUris(NuGetVersion clientVersion, params string[] orderedTypes)
{
if (clientVersion == null)
{
throw new ArgumentNullException(nameof(clientVersion));
}
return GetServiceEntries(clientVersion, orderedTypes).Select(e => e.Uri).ToList();
}
private static IDictionary<string, List<ServiceIndexEntry>> MakeLookup(ServiceIndexModel index, PackageSource? packageSource)
{
var result = new Dictionary<string, List<ServiceIndexEntry>>(StringComparer.Ordinal);
if (index?.Resources is null)
{
return result;
}
foreach (var resource in index.Resources)
{
var id = resource.Id;
if (string.IsNullOrEmpty(id) || !Uri.TryCreate(id, UriKind.Absolute, out Uri? uri))
{
continue;
}
if (packageSource != null && uri.Scheme == Uri.UriSchemeHttp && packageSource.IsHttps)
{
ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticServiceIndexEntryEvent(source: packageSource.Source, httpsSourceHasHttpResource: true));
}
var clientVersions = new List<SemanticVersion>();
if (resource.ClientVersion is null)
{
clientVersions.Add(_defaultVersion);
}
else
{
foreach (var versionString in resource.ClientVersion)
{
if (SemanticVersion.TryParse(versionString, out SemanticVersion? semVer))
{
clientVersions.Add(semVer);
}
}
}
foreach (var type in resource.Type)
{
foreach (var clientVersion in clientVersions)
{
if (!result.TryGetValue(type, out List<ServiceIndexEntry>? entries))
{
entries = new List<ServiceIndexEntry>();
result.Add(type, entries);
}
entries.Add(new ServiceIndexEntry(uri, type, clientVersion));
}
}
}
#if NET8_0_OR_GREATER
foreach (var type in result.Keys)
#else
foreach (var type in result.Keys.ToArray())
#endif
{
result[type] = result[type].OrderByDescending(e => e.ClientVersion).ToList();
}
return result;
}
private static IDictionary<string, List<ServiceIndexEntry>> MakeLookup(JObject index, PackageSource? packageSource)
{
var result = new Dictionary<string, List<ServiceIndexEntry>>(StringComparer.Ordinal);
JToken? resources;
if (index.TryGetValue("resources", out resources))
{
foreach (var resource in resources)
{
var id = GetValues(resource["@id"]).SingleOrDefault();
Uri? uri;
if (string.IsNullOrEmpty(id) || !Uri.TryCreate(id, UriKind.Absolute, out uri))
{
// Skip invalid or missing @ids
continue;
}
// Capture if the resource is http & the source is https
if (packageSource != null && uri.Scheme == Uri.UriSchemeHttp && packageSource.IsHttps)
{
ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticServiceIndexEntryEvent(source: packageSource.Source, httpsSourceHasHttpResource: true));
}
var types = GetValues(resource["@type"]).ToArray();
var clientVersionToken = resource["clientVersion"];
var clientVersions = new List<SemanticVersion>();
if (clientVersionToken == null)
{
// For non-versioned services assume all clients are compatible
clientVersions.Add(_defaultVersion);
}
else
{
// Parse supported versions
foreach (var versionString in GetValues(clientVersionToken))
{
SemanticVersion? version;
if (SemanticVersion.TryParse(versionString, out version))
{
clientVersions.Add(version);
}
}
}
// Create service entries
foreach (var type in types)
{
foreach (var version in clientVersions)
{
List<ServiceIndexEntry>? entries;
if (!result.TryGetValue(type, out entries))
{
entries = new List<ServiceIndexEntry>();
result.Add(type, entries);
}
entries.Add(new ServiceIndexEntry(uri, type, version));
}
}
}
}
// Order versions desc for faster lookup later.
foreach (var type in result.Keys.ToArray())
{
result[type] = result[type].OrderByDescending(e => e.ClientVersion).ToList();
}
return result;
}
/// <summary>
/// Read string values from an array or string.
/// Returns an empty enumerable if the value is null.
/// </summary>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Only called from JObject constructor which is annotated with [RUC]/[RDC].")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Only called from JObject constructor which is annotated with [RUC]/[RDC].")]
#endif
private static IEnumerable<string> GetValues(JToken? token)
{
if (token?.Type == JTokenType.Array)
{
foreach (var entry in token)
{
if (entry.Type == JTokenType.String)
{
yield return entry.ToObject<string>()!;
}
}
}
else if (token?.Type == JTokenType.String)
{
yield return token.ToObject<string>()!;
}
}
}
}
|