File: RuntimeModel\JsonRuntimeFormat.cs
Web Access
Project: NuGet.Packaging.csproj (NuGet.Packaging)
// 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;
using System.IO;
using System.Linq;
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NuGet.Frameworks;
using NuGet.Shared;
using NuGet.Versioning;
using JsonException = System.Text.Json.JsonException;

namespace NuGet.RuntimeModel
{
    public static class JsonRuntimeFormat
    {
        /// <summary>
        /// Reads a runtime graph from a UTF-8 file with or without a byte order mark.
        /// </summary>
        public static RuntimeGraph ReadRuntimeGraph(string filePath)
        {
            using (var fileStream = File.OpenRead(filePath))
            {
                return ReadRuntimeGraph(fileStream);
            }
        }

        /// <summary>
        /// Reads a runtime graph from a UTF-8 stream with or without a byte order mark.
        /// The stream will be disposed at the end of reading.
        /// </summary>
        public static RuntimeGraph ReadRuntimeGraph(Stream stream)
        {
            using (stream)
            {
                return ReadUtf8RuntimeGraph(stream);
            }
        }

        private static RuntimeGraph ReadUtf8RuntimeGraph(Stream stream)
        {
            var reader = new Utf8JsonStreamReader(stream);
            try
            {
                return ReadRuntimeGraph(ref reader);
            }
            finally
            {
                reader.Dispose();
            }
        }

        /// <summary>
        /// Reads a runtime graph from a JSON stream in a single pass.
        /// </summary>
        private static RuntimeGraph ReadRuntimeGraph(ref Utf8JsonStreamReader reader)
        {
            if (reader.TokenType == JsonTokenType.None)
            {
                reader.Read();
            }

            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            List<RuntimeDescription>? runtimes = null;
            List<CompatibilityProfile>? supports = null;

            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
            {
                if (reader.TokenType != JsonTokenType.PropertyName)
                {
                    throw new JsonException();
                }

                bool isRuntimes = reader.ValueTextEquals("runtimes"u8);
                bool isSupports = reader.ValueTextEquals("supports"u8);
                ReadValue(ref reader);

                if (isRuntimes)
                {
                    runtimes = ReadRuntimeDescriptions(ref reader);
                }
                else if (isSupports)
                {
                    supports = ReadCompatibilityProfiles(ref reader);
                }
                else
                {
                    reader.Skip();
                }
            }

            if (reader.TokenType != JsonTokenType.EndObject
                || reader.Read())
            {
                throw new JsonException();
            }

            return new RuntimeGraph(
                runtimes is null ? Array.Empty<RuntimeDescription>() : runtimes,
                supports is null ? Array.Empty<CompatibilityProfile>() : supports);
        }

        private static List<RuntimeDescription> ReadRuntimeDescriptions(ref Utf8JsonStreamReader reader)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                reader.Skip();
                return [];
            }

            List<RuntimeDescription> values = [];

            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
            {
                string runtimeIdentifier = ReadPropertyName(ref reader);
                ReadValue(ref reader);
                RuntimeDescription? runtime = ReadRuntimeDescription(ref reader, runtimeIdentifier);
                if (runtime is null)
                {
                    throw new JsonException();
                }

                values.Add(runtime);
            }

            if (reader.TokenType != JsonTokenType.EndObject)
            {
                throw new JsonException();
            }

            return values;
        }

        private static RuntimeDescription? ReadRuntimeDescription(
            ref Utf8JsonStreamReader reader,
            string runtimeIdentifier)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                reader.Skip();
                return new RuntimeDescription(runtimeIdentifier);
            }

            List<string>? inheritedRuntimes = null;
            List<RuntimeDependencySet> dependencySets = [];

            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
            {
                if (reader.TokenType == JsonTokenType.PropertyName
                    && reader.ValueTextEquals("#import"u8))
                {
                    ReadValue(ref reader);
                    inheritedRuntimes = ReadStringArray(ref reader);
                    if (inheritedRuntimes is null)
                    {
                        return null;
                    }
                }
                else
                {
                    string propertyName = ReadPropertyName(ref reader);
                    ReadValue(ref reader);
                    RuntimeDependencySet? dependencySet = ReadRuntimeDependencySet(ref reader, propertyName);
                    if (dependencySet is null)
                    {
                        return null;
                    }

                    dependencySets.Add(dependencySet);
                }
            }

            if (reader.TokenType != JsonTokenType.EndObject)
            {
                return null;
            }

            return new RuntimeDescription(runtimeIdentifier, inheritedRuntimes, dependencySets);
        }

        private static RuntimeDependencySet? ReadRuntimeDependencySet(
            ref Utf8JsonStreamReader reader,
            string dependencySetId)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                reader.Skip();
                return new RuntimeDependencySet(dependencySetId);
            }

            List<RuntimePackageDependency> dependencies = [];

            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
            {
                string packageId = ReadPropertyName(ref reader);
                string? versionRange = ReadNextScalarAsString(ref reader);
                reader.Skip();
                if (versionRange is null)
                {
                    return null;
                }

                dependencies.Add(new RuntimePackageDependency(packageId, VersionRange.Parse(versionRange)));
            }

            if (reader.TokenType != JsonTokenType.EndObject)
            {
                return null;
            }

            return new RuntimeDependencySet(dependencySetId, dependencies);
        }

        private static List<CompatibilityProfile> ReadCompatibilityProfiles(ref Utf8JsonStreamReader reader)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                reader.Skip();
                return [];
            }

            List<CompatibilityProfile> values = [];

            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
            {
                string profileName = ReadPropertyName(ref reader);
                ReadValue(ref reader);
                CompatibilityProfile? profile = ReadCompatibilityProfile(ref reader, profileName);
                if (profile is null)
                {
                    throw new JsonException();
                }

                values.Add(profile);
            }

            if (reader.TokenType != JsonTokenType.EndObject)
            {
                throw new JsonException();
            }

            return values;
        }

        private static CompatibilityProfile? ReadCompatibilityProfile(
            ref Utf8JsonStreamReader reader,
            string profileName)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                reader.Skip();
                return new CompatibilityProfile(profileName);
            }

            List<FrameworkRuntimePair> restoreContexts = [];

            while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
            {
                string frameworkName = ReadPropertyName(ref reader);
                ReadValue(ref reader);
                List<FrameworkRuntimePair>? pairs = ReadFrameworkRuntimePairs(
                    ref reader,
                    NuGetFramework.Parse(frameworkName));
                if (pairs is null)
                {
                    return null;
                }

                restoreContexts.AddRange(pairs);
            }

            if (reader.TokenType != JsonTokenType.EndObject)
            {
                return null;
            }

            return new CompatibilityProfile(profileName, restoreContexts);
        }

        private static List<FrameworkRuntimePair>? ReadFrameworkRuntimePairs(
            ref Utf8JsonStreamReader reader,
            NuGetFramework framework)
        {
            var values = new List<FrameworkRuntimePair>();
            if (reader.TokenType == JsonTokenType.String)
            {
                values.Add(new FrameworkRuntimePair(framework, reader.GetString()));
            }
            else if (reader.TokenType == JsonTokenType.StartArray)
            {
                List<string?>? runtimeIdentifiers = ReadScalarArrayAsStrings(ref reader);
                if (runtimeIdentifiers is not null)
                {
                    foreach (string? runtimeIdentifier in runtimeIdentifiers)
                    {
                        values.Add(new FrameworkRuntimePair(framework, runtimeIdentifier));
                    }
                }
            }
            else
            {
                reader.Skip();
            }

            return values;
        }

        private static List<string>? ReadStringArray(ref Utf8JsonStreamReader reader)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                reader.Skip();
                return null;
            }

            List<string?>? values = ReadScalarArrayAsStrings(ref reader);
            if (values is null)
            {
                return [];
            }

            // RuntimeDescription historically permits null entries despite its non-nullable public contract.
            return values.ConvertAll(value => value!);
        }

        private static string? ReadNextScalarAsString(ref Utf8JsonStreamReader reader)
        {
            if (!reader.Read())
            {
                throw new JsonException();
            }

            return reader.ReadScalarAsInvariantString();
        }

        private static List<string?>? ReadScalarArrayAsStrings(ref Utf8JsonStreamReader reader)
        {
            List<string?>? values = null;
            while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
            {
                values ??= [];
                values.Add(reader.ReadScalarAsInvariantString());
            }

            if (reader.TokenType != JsonTokenType.EndArray)
            {
                throw new JsonException();
            }

            return values;
        }

        private static string ReadPropertyName(ref Utf8JsonStreamReader reader)
        {
            return reader.TokenType == JsonTokenType.PropertyName
                ? reader.GetString() ?? throw new JsonException()
                : throw new JsonException();
        }

        private static void ReadValue(ref Utf8JsonStreamReader reader)
        {
            if (!reader.Read())
            {
                throw new JsonException();
            }
        }

        [Obsolete("Use ReadRuntimeGraph(Stream) instead.")]
        public static RuntimeGraph ReadRuntimeGraph(TextReader textReader)
        {
            var loadSettings = new JsonLoadSettings()
            {
                LineInfoHandling = LineInfoHandling.Ignore,
                CommentHandling = CommentHandling.Ignore
            };

            using (var jsonReader = new JsonTextReader(textReader))
            {
                return ReadRuntimeGraph(JToken.Load(jsonReader, loadSettings));
            }
        }

        public static void WriteRuntimeGraph(string filePath, RuntimeGraph runtimeGraph)
        {
            using (var fileStream = new FileStream(filePath, FileMode.Create))
            using (var textWriter = new StreamWriter(fileStream))
            using (var jsonWriter = new JsonTextWriter(textWriter))
            using (var writer = new JsonObjectWriter(jsonWriter))
            {
                jsonWriter.Formatting = Formatting.Indented;
                jsonWriter.WriteStartObject();
                WriteRuntimeGraph(writer, runtimeGraph);
                jsonWriter.WriteEndObject();
            }
        }

        public static RuntimeGraph ReadRuntimeGraph(JToken json)
        {
            return new RuntimeGraph(
                EachProperty(json["runtimes"]).Select(ReadRuntimeDescription),
                EachProperty(json["supports"]).Select(ReadCompatibilityProfile));
        }

        public static void WriteRuntimeGraph(IObjectWriter writer, RuntimeGraph runtimeGraph)
        {
            if (runtimeGraph != null)
            {
                if (runtimeGraph.Runtimes.Any() == true)
                {
                    writer.WriteObjectStart("runtimes");

                    IOrderedEnumerable<RuntimeDescription> sortedRuntimes = runtimeGraph.Runtimes.Values
                        .OrderBy(runtime => runtime.RuntimeIdentifier, StringComparer.Ordinal);

                    foreach (RuntimeDescription runtime in sortedRuntimes)
                    {
                        WriteRuntimeDescription(writer, runtime);
                    }

                    writer.WriteObjectEnd();
                }

                if (runtimeGraph.Supports.Any() == true)
                {
                    writer.WriteObjectStart("supports");

                    IOrderedEnumerable<CompatibilityProfile> sortedSupports = runtimeGraph.Supports.Values
                        .OrderBy(runtime => runtime.Name, StringComparer.Ordinal);

                    foreach (CompatibilityProfile support in sortedSupports)
                    {
                        WriteCompatibilityProfile(writer, support);
                    }

                    writer.WriteObjectEnd();
                }
            }
        }

        private static void WriteRuntimeDescription(IObjectWriter writer, RuntimeDescription data)
        {
            writer.WriteObjectStart(data.RuntimeIdentifier);

            writer.WriteNameArray("#import", data.InheritedRuntimes);

            var sortedDependencySets = data.RuntimeDependencySets
                                           .OrderBy(pair => pair.Key, StringComparer.Ordinal)
                                           .Select(pair => pair.Value);

            foreach (var set in sortedDependencySets)
            {
                WriteRuntimeDependencySet(writer, set);
            }

            writer.WriteObjectEnd();
        }

        private static void WriteRuntimeDependencySet(IObjectWriter writer, RuntimeDependencySet data)
        {
            writer.WriteObjectStart(data.Id);

            var sortedDependencies = data.Dependencies
                                         .OrderBy(pair => pair.Key, StringComparer.Ordinal)
                                         .Select(pair => pair.Value);

            foreach (var dependency in sortedDependencies)
            {
                WritePackageDependency(writer, dependency);
            }

            writer.WriteObjectEnd();
        }

        private static void WritePackageDependency(IObjectWriter writer, RuntimePackageDependency data)
        {
            writer.WriteNameValue(data.Id, data.VersionRange.ToNormalizedString());
        }

        private static void WriteCompatibilityProfile(IObjectWriter writer, CompatibilityProfile data)
        {
            writer.WriteObjectStart(data.Name);

            var frameworkGroups = data.RestoreContexts.GroupBy(context => context.Framework);

            foreach (var frameworkGroup in frameworkGroups)
            {
                var name = frameworkGroup.Key.GetShortFolderName();
                var runtimes = frameworkGroup.ToList();
                if (runtimes.Count == 1)
                {
                    // Write a string
                    writer.WriteNameValue(name, runtimes[0].RuntimeIdentifier);
                }
                else if (runtimes.Count > 0)
                {
                    writer.WriteNameArray(name, runtimes.Select(rt => rt.RuntimeIdentifier));
                }
            }

            writer.WriteObjectEnd();
        }

        private static CompatibilityProfile ReadCompatibilityProfile(KeyValuePair<string, JToken> json)
        {
            var name = json.Key;
            var sets = new List<FrameworkRuntimePair>();
            foreach (var property in EachProperty(json.Value))
            {
                var profiles = ReadCompatibilitySets(property);
                sets.AddRange(profiles);
            }
            return new CompatibilityProfile(name, sets);
        }

        private static IEnumerable<FrameworkRuntimePair> ReadCompatibilitySets(KeyValuePair<string, JToken> property)
        {
            var framework = NuGetFramework.Parse(property.Key);
            switch (property.Value.Type)
            {
                case JTokenType.Array:
                    foreach (var value in (JArray)property.Value)
                    {
                        yield return new FrameworkRuntimePair(framework, value.Value<string>());
                    }
                    break;
                case JTokenType.String:
                    yield return new FrameworkRuntimePair(framework, property.Value.ToString());
                    break;
                    // Other token types are not supported
            }
        }

        private static RuntimeDescription ReadRuntimeDescription(KeyValuePair<string, JToken> json)
        {
            var name = json.Key;
            List<string>? inheritedRuntimes = null;
            List<RuntimeDependencySet>? additionalDependencies = null;
            foreach (var property in EachProperty(json.Value))
            {
                if (property.Key == "#import")
                {
                    var imports = (JArray)property.Value;
                    foreach (var import in imports)
                    {
                        inheritedRuntimes ??= new List<string>(imports.Count);
                        inheritedRuntimes.Add(import.Value<string>()!);
                    }
                }
                else
                {
                    var dependency = ReadRuntimeDependencySet(property);
                    additionalDependencies ??= new();
                    additionalDependencies.Add(dependency);
                }
            }
            return new RuntimeDescription(name, inheritedRuntimes, additionalDependencies);
        }

        private static RuntimeDependencySet ReadRuntimeDependencySet(KeyValuePair<string, JToken> json)
        {
            return new RuntimeDependencySet(
                json.Key,
                EachProperty(json.Value).Select(ReadRuntimePackageDependency));
        }

        private static RuntimePackageDependency ReadRuntimePackageDependency(KeyValuePair<string, JToken> json)
        {
            return new RuntimePackageDependency(json.Key, VersionRange.Parse(json.Value.Value<string>()!));
        }

        private static IEnumerable<KeyValuePair<string, JToken>> EachProperty(JToken? json)
        {
            return (json as IEnumerable<KeyValuePair<string, JToken>>)
                   ?? Enumerable.Empty<KeyValuePair<string, JToken>>();
        }
    }
}