File: DependencyGraphSpec.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.ProjectModel\NuGet.ProjectModel.csproj (NuGet.ProjectModel)
// 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.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using Newtonsoft.Json;
using NuGet.Common;
using NuGet.Packaging;
using NuGet.Shared;

namespace NuGet.ProjectModel
{
    public class DependencyGraphSpec
    {
        /// <summary>
        /// Allows a user to enable the legacy SHA512 hash function for dgSpec files which is used by no-op.
        /// </summary>
        private static bool? UseLegacyHashFunction;

        private const string DGSpecFileNameExtension = "{0}.nuget.dgspec.json";

        private readonly SortedSet<string> _restore = new(PathUtility.GetStringComparerBasedOnOS());
        private readonly SortedDictionary<string, PackageSpec> _projects = new(PathUtility.GetStringComparerBasedOnOS());

        private const int Version = 1;

        private readonly bool _isReadOnly;

        private static readonly byte[] RestorePropertyName = Encoding.UTF8.GetBytes("restore");
        private static readonly byte[] ProjectsPropertyName = Encoding.UTF8.GetBytes("projects");

        public static string GetDGSpecFileName(string projectName)
        {
            return string.Format(CultureInfo.InvariantCulture, DGSpecFileNameExtension, projectName);
        }

        public DependencyGraphSpec()
            : this(isReadOnly: false)
        {
        }

        public DependencyGraphSpec(bool isReadOnly)
            : this(isReadOnly, EnvironmentVariableWrapper.Instance)
        {
        }

        internal DependencyGraphSpec(bool isReadOnly, IEnvironmentVariableReader environmentVariableReader)
        {
            _isReadOnly = isReadOnly;
            UseLegacyHashFunction ??= string.Equals(environmentVariableReader.GetEnvironmentVariable("NUGET_ENABLE_LEGACY_DGSPEC_HASH_FUNCTION"), bool.TrueString, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Projects to restore.
        /// </summary>
        public IReadOnlyList<string> Restore => _restore.ToList();

        /// <summary>
        /// All project specs.
        /// </summary>
        public IReadOnlyList<PackageSpec> Projects => _projects.Values.ToList();

        public PackageSpec GetProjectSpec(string projectUniqueName)
        {
            if (projectUniqueName == null)
            {
                throw new ArgumentNullException(nameof(projectUniqueName));
            }

            PackageSpec project;
            _projects.TryGetValue(projectUniqueName, out project);

            return project;
        }

        public IReadOnlyList<string> GetParents(string rootUniqueName)
        {
            var parents = new List<PackageSpec>();

            foreach (var project in Projects)
            {
                if (!StringComparer.OrdinalIgnoreCase.Equals(
                    project.RestoreMetadata.ProjectUniqueName,
                    rootUniqueName))
                {
                    var closure = GetClosure(project.RestoreMetadata.ProjectUniqueName);

                    if (closure.Any(e => StringComparer.OrdinalIgnoreCase.Equals(
                        e.RestoreMetadata.ProjectUniqueName,
                        rootUniqueName)))
                    {
                        parents.Add(project);
                    }
                }
            }

            return parents
                .Select(e => e.RestoreMetadata.ProjectUniqueName)
                .ToList();
        }

        /// <summary>
        /// Retrieve a DependencyGraphSpec with the project closure.
        /// </summary>
        /// <param name="projectUniqueName"></param>
        /// <returns></returns>
        public DependencyGraphSpec WithProjectClosure(string projectUniqueName)
        {
            var projectDependencyGraphSpec = new DependencyGraphSpec();
            projectDependencyGraphSpec.AddRestore(projectUniqueName);
            foreach (var spec in GetClosure(projectUniqueName))
            {
                // Clone the PackageSpec unless the caller has indicated that the objects won't be modified
                projectDependencyGraphSpec.AddProject(_isReadOnly ? spec : spec.Clone());
            }

            return projectDependencyGraphSpec;
        }

        /// <summary>
        /// Creates a new <see cref="DependencyGraphSpec" /> from a project name and project closure.
        /// </summary>
        /// <param name="projectUniqueName">The project's unique name.</param>
        /// <param name="closure">The project's closure</param>
        /// <returns>A <see cref="DependencyGraphSpec" />.</returns>
        /// <exception cref="ArgumentException">Thrown if <paramref name="projectUniqueName" />
        /// is <see langword="null" /> or an empty string.</exception>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="closure" /> is <see langword="null" />.</exception>
        public DependencyGraphSpec CreateFromClosure(string projectUniqueName, IReadOnlyList<PackageSpec> closure)
        {
            if (string.IsNullOrEmpty(projectUniqueName))
            {
                throw new ArgumentException(Strings.ArgumentNullOrEmpty, nameof(projectUniqueName));
            }

            if (closure == null)
            {
                throw new ArgumentNullException(nameof(closure));
            }

            var dgSpec = new DependencyGraphSpec();

            dgSpec.AddRestore(projectUniqueName);

            foreach (PackageSpec packageSpec in closure)
            {
                dgSpec.AddProject(_isReadOnly ? packageSpec : packageSpec.Clone());
            }

            return dgSpec;
        }

        /// <summary>
        /// Retrieve the full project closure including the root project itself.
        /// </summary>
        /// <remarks>Results are not sorted in any form.</remarks>
        public IReadOnlyList<PackageSpec> GetClosure(string rootUniqueName)
        {
            if (rootUniqueName == null)
            {
                throw new ArgumentNullException(nameof(rootUniqueName));
            }

            var closure = new List<PackageSpec>();

            var added = new SortedSet<string>(PathUtility.GetStringComparerBasedOnOS());
            var toWalk = new Stack<PackageSpec>();

            // Start with the root
            toWalk.Push(GetProjectSpec(rootUniqueName));

            while (toWalk.Count > 0)
            {
                var spec = toWalk.Pop();

                if (spec != null)
                {
                    // Add every spec to the closure
                    closure.Add(spec);

                    // Find children
                    foreach (var projectName in GetProjectReferenceNames(spec, _projects))
                    {
                        if (added.Add(projectName))
                        {
                            toWalk.Push(GetProjectSpec(projectName));
                        }
                    }
                }
            }

            return closure;
        }

        private static IEnumerable<string> GetProjectReferenceNames(PackageSpec spec, SortedDictionary<string, PackageSpec> projectsByUniqueName)
        {
            // Handle projects which may not have specs, and which may not have references
            return spec?.RestoreMetadata?
                .TargetFrameworks
                .SelectMany(e => e.ProjectReferences)
                .Where(project => projectsByUniqueName.ContainsKey(project.ProjectUniqueName))
                .Select(project => project.ProjectUniqueName)
                ?? Enumerable.Empty<string>();
        }

        public void AddRestore(string projectUniqueName)
        {
            _restore.Add(projectUniqueName);
        }

        public void AddProject(PackageSpec projectSpec)
        {
            // Find the unique name in the spec, otherwise generate a new one.
            string projectUniqueName = projectSpec.RestoreMetadata?.ProjectUniqueName
                ?? Guid.NewGuid().ToString();

            if (!_projects.ContainsKey(projectUniqueName))
            {
                _projects.Add(projectUniqueName, projectSpec);
            }
        }

        public static DependencyGraphSpec Union(IEnumerable<DependencyGraphSpec> dgSpecs)
        {
            var projects =
                dgSpecs.SelectMany(e => e.Projects)
                    .GroupBy(e => e.RestoreMetadata.ProjectUniqueName, PathUtility.GetStringComparerBasedOnOS())
                    .Select(e => e.First())
                    .ToList();

            var newDgSpec = new DependencyGraphSpec();
            foreach (var project in projects)
            {
                newDgSpec.AddProject(project);
            }
            return newDgSpec;
        }

        public static DependencyGraphSpec Load(string path)
        {
            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
            if (stream.Length == 0)
            {
                throw new InvalidDataException();
            }

            var jsonReader = new Utf8JsonStreamReader(stream);
            var dgspec = new DependencyGraphSpec();
            bool wasObjectRead = false;

            try
            {
                if (jsonReader.TokenType == JsonTokenType.StartObject)
                {
                    wasObjectRead = true;
                    while (jsonReader.Read() && jsonReader.TokenType == JsonTokenType.PropertyName)
                    {
                        if (jsonReader.ValueTextEquals(RestorePropertyName))
                        {
                            jsonReader.Read();
                            ParseRestoreSection(dgspec, ref jsonReader);
                        }
                        else if (jsonReader.ValueTextEquals(ProjectsPropertyName))
                        {
                            jsonReader.Read();
                            ParseProjectsSection(dgspec, ref jsonReader, path);
                        }
                        else
                        {
                            jsonReader.Skip();
                        }
                    }
                }
            }
            catch (System.Text.Json.JsonException ex)
            {
                throw FileFormatException.Create(ex, path);
            }

            if (!wasObjectRead || jsonReader.TokenType != JsonTokenType.EndObject || jsonReader.CurrentDepth != 0)
            {
                throw new InvalidDataException();
            }

            return dgspec;
        }

        private static void ParseRestoreSection(DependencyGraphSpec dgspec, ref Utf8JsonStreamReader jsonReader)
        {
            if (jsonReader.TokenType == JsonTokenType.StartObject)
            {
                while (jsonReader.Read() && jsonReader.TokenType == JsonTokenType.PropertyName)
                {
                    var restoreProjectName = jsonReader.GetString();
                    dgspec._restore.Add(restoreProjectName);

                    // restore section is an object where each property name is the project, and the value
                    // is an empty object. We need to skip the object so the reader is ready for the
                    // next property name
                    jsonReader.Skip();
                }
            }
        }

        private static void ParseProjectsSection(DependencyGraphSpec dgspec, ref Utf8JsonStreamReader jsonReader, string path)
        {
            if (jsonReader.TokenType == JsonTokenType.StartObject)
            {
                while (jsonReader.Read() && jsonReader.TokenType == JsonTokenType.PropertyName)
                {
                    var projectName = jsonReader.GetString();
                    jsonReader.Read();
                    var packageSpec = JsonPackageSpecReader.GetPackageSpec(ref jsonReader, name: null, path, EnvironmentVariableWrapper.Instance);
                    dgspec._projects.Add(projectName, packageSpec);
                }
            }
        }

        public void Save(string path)
        {
            using (var fileStream = new FileStream(path, FileMode.Create))
            {
                Save(fileStream);
            }
        }

        public void Save(Stream stream)
        {
#if NET5_0_OR_GREATER
            using (var textWriter = new StreamWriter(stream))
#else
            using (var textWriter = new NoAllocNewLineStreamWriter(stream))
#endif
            using (var jsonWriter = new JsonTextWriter(textWriter))
            using (var writer = new RuntimeModel.JsonObjectWriter(jsonWriter))
            {
                jsonWriter.Formatting = Formatting.Indented;

                Write(writer, hashing: false, PackageSpecWriter.Write);
            }
        }

        public string GetHash()
        {
            // Use the faster FNV hash function for hashing unless the user has specified to use the legacy SHA512 hash function
            return GetHash(() => UseLegacyHashFunction == true ? new Sha512HashFunction() : new FnvHash64Function());
        }

        internal string GetHash(Func<IHashFunction> getHashFunction)
        {
            if (getHashFunction == null)
            {
                throw new ArgumentNullException(nameof(getHashFunction));
            }

            using IHashFunction hashFunc = getHashFunction();
            using var writer = new HashObjectWriter(hashFunc);
            Write(writer, hashing: true, PackageSpecWriter.Write);
            return writer.GetHash();
        }

        private void Write(RuntimeModel.IObjectWriter writer, bool hashing, Action<PackageSpec, RuntimeModel.IObjectWriter, bool, IEnvironmentVariableReader> writeAction)
        {
            writer.WriteObjectStart();
            writer.WriteNameValue("format", Version);

            writer.WriteObjectStart("restore");

            // Preserve default sort order
            foreach (var restoreName in _restore)
            {
                writer.WriteObjectStart(restoreName);
                writer.WriteObjectEnd();
            }

            writer.WriteObjectEnd();

            writer.WriteObjectStart("projects");

            // Preserve default sort order
            foreach (var pair in _projects)
            {
                var project = pair.Value;

                writer.WriteObjectStart(project.RestoreMetadata.ProjectUniqueName);
                writeAction.Invoke(project, writer, hashing, EnvironmentVariableWrapper.Instance);
                writer.WriteObjectEnd();
            }

            writer.WriteObjectEnd();
            writer.WriteObjectEnd();
        }

        /// <summary>
        /// Order dependencies by children first.
        /// </summary>
        public static IReadOnlyList<PackageSpec> SortPackagesByDependencyOrder(
            IEnumerable<PackageSpec> packages)
        {
            return TopologicalSortUtility.SortPackagesByDependencyOrder(
                items: packages,
                comparer: PathUtility.GetStringComparerBasedOnOS(),
                getId: GetPackageSpecId,
                getDependencies: GetPackageSpecDependencyIds);
        }

        public DependencyGraphSpec WithoutRestores()
        {
            var newSpec = new DependencyGraphSpec();

            foreach (var project in Projects)
            {
                newSpec.AddProject(project);
            }

            return newSpec;
        }

        public DependencyGraphSpec WithReplacedSpec(PackageSpec project)
        {
            var newSpec = new DependencyGraphSpec();
            newSpec.AddProject(project);
            newSpec.AddRestore(project.RestoreMetadata.ProjectUniqueName);

            foreach (var child in Projects)
            {
                newSpec.AddProject(child);
            }

            return newSpec;
        }

        public DependencyGraphSpec WithPackageSpecs(IEnumerable<PackageSpec> packageSpecs)
        {
            var newSpec = new DependencyGraphSpec();

            foreach (var packageSpec in packageSpecs)
            {
                newSpec.AddProject(packageSpec);
                newSpec.AddRestore(packageSpec.RestoreMetadata.ProjectUniqueName);
            }

            foreach (var child in Projects)
            {
                newSpec.AddProject(child);
            }

            return newSpec;
        }

        public DependencyGraphSpec WithoutTools()
        {
            var newSpec = new DependencyGraphSpec();

            foreach (var project in Projects)
            {
                if (project.RestoreMetadata.ProjectStyle != ProjectStyle.DotnetCliTool)
                {
                    // Add all non-tool projects
                    newSpec.AddProject(project);

                    // Add to restore if it existed in the current dg file
                    if (_restore.Contains(project.RestoreMetadata.ProjectUniqueName))
                    {
                        newSpec.AddRestore(project.RestoreMetadata.ProjectUniqueName);
                    }
                }
            }

            return newSpec;
        }

        /// <summary>
        /// PackageSpec -> id
        /// </summary>
        private static string GetPackageSpecId(PackageSpec spec)
        {
            return spec.RestoreMetadata.ProjectUniqueName;
        }

        /// <summary>
        /// PackageSpec -> Project dependency ids
        /// </summary>
        private static string[] GetPackageSpecDependencyIds(PackageSpec spec)
        {
            return spec.RestoreMetadata
                .TargetFrameworks
                .SelectMany(r => r.ProjectReferences)
                .Select(r => r.ProjectUniqueName)
                .Distinct(PathUtility.GetStringComparerBasedOnOS())
                .ToArray();
        }
    }
}