File: Resources\ServiceIndexResourceV3.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol)
// 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.

#nullable disable

using System;
using System.Collections.Generic;
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);

        internal ServiceIndexResourceV3(JObject index, DateTime requestTime, PackageSource packageSource)
        {
            _json = index.ToString();
            _index = MakeLookup(index, packageSource);
            _requestTime = requestTime;
        }

        public ServiceIndexResourceV3(JObject index, DateTime requestTime) : this(index, requestTime, null) { }

        internal ServiceIndexResourceV3(ServiceIndexModel model, DateTime requestTime, PackageSource packageSource)
        {
            _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();
        }

#nullable enable
        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;
        }

#nullable disable

        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>
        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>();
            }
        }
    }
}