File: LockFile\LockFileFormat.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 Newtonsoft.Json;
using NuGet.Common;
using NuGet.RuntimeModel;

namespace NuGet.ProjectModel
{
    public class LockFileFormat
    {
        public static readonly int Version = 4;

        /// <summary>
        /// The assets file version that supports the aliased format.
        /// This means, the targets section and project section are using the alias as a pivot when there's multiple frameworks instead of the framework name.
        /// </summary>
        public const int AliasedVersion = 4;

        /// <summary>
        /// The assets file version that is used for classic csproj, or legacy project PackageReference.
        /// This is also the assets file version used when the .NET SDK used for the project is not 10.0.300 or newer.
        /// </summary>
        public const int LegacyVersion = 3;

        // If this is ever renamed, you should also rename NoOpRestoreUtilities.NoOpCacheFileName to keep them in sync.
        public static readonly string AssetsFileName = "project.assets.json";

        private const string VersionProperty = "version";
        private const string LibrariesProperty = "libraries";
        private const string TargetsProperty = "targets";
        private const string ProjectFileDependencyGroupsProperty = "projectFileDependencyGroups";
        private const string ServicableProperty = "servicable";
        private const string Sha512Property = "sha512";
        private const string FilesProperty = "files";
        private const string HasToolsProperty = "hasTools";
        private const string DependenciesProperty = "dependencies";
        private const string FrameworkAssembliesProperty = "frameworkAssemblies";
        private const string RuntimeProperty = "runtime";
        private const string CompileProperty = "compile";
        private const string NativeProperty = "native";
        private const string BuildProperty = "build";
        private const string BuildMultiTargetingProperty = "buildMultiTargeting";
        private const string ContentFilesProperty = "contentFiles";
        private const string RuntimeTargetsProperty = "runtimeTargets";
        private const string ResourceProperty = "resource";
        private const string TypeProperty = "type";
        private const string PathProperty = "path";
        private const string MSBuildProjectProperty = "msbuildProject";
        private const string FrameworkProperty = "framework";
        private const string ToolsProperty = "tools";
        private const string PackageFoldersProperty = "packageFolders";
        private const string PackageSpecProperty = "project";
        internal const string LogsProperty = "logs";
        private const string EmbedProperty = "embed";
        private const string FrameworkReferencesProperty = "frameworkReferences";
        private const string CentralTransitiveDependencyGroupsProperty = "centralTransitiveDependencyGroups";

        public LockFile Parse(string lockFileContent, string path)
        {
            return Parse(lockFileContent, NullLogger.Instance, path);
        }

        public LockFile Parse(string lockFileContent, ILogger log, string path)
        {
            byte[] byteArray = Encoding.UTF8.GetBytes(lockFileContent);
            using (var stream = new MemoryStream(byteArray))
            {
                return Read(stream, log, path);
            }
        }

        public LockFile Read(string filePath)
        {
            return Read(filePath, NullLogger.Instance);
        }

        public LockFile Read(string filePath, ILogger log)
        {
            using (var stream = File.OpenRead(filePath))
            {
                return Read(stream, log, filePath);
            }
        }

        public LockFile Read(Stream stream, string path)
        {
            return Read(stream, NullLogger.Instance, path);
        }

#pragma warning disable CA1822 // Mark members as static - public API
        public LockFile Read(Stream stream, ILogger log, string path)
#pragma warning restore CA1822 // Mark members as static
        {
            return Read(stream, log, path, flags: LockFileReadFlags.All);
        }

        internal static LockFile Read(Stream stream, ILogger log, string path, LockFileReadFlags flags)
        {
            return Utf8JsonRead(stream, log, path, flags);
        }

        public void Write(string filePath, LockFile lockFile)
        {
            // Create the directory if it does not exist
            var fileInfo = new FileInfo(filePath);
            fileInfo.Directory.Create();

            using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
            {
                Write(stream, lockFile);
            }
        }

        public void Write(Stream stream, LockFile lockFile)
        {
#if NET5_0_OR_GREATER
            using (var textWriter = new StreamWriter(stream))
#else
            using (var textWriter = new NoAllocNewLineStreamWriter(stream))
#endif
            {
                Write(textWriter, lockFile);
            }
        }

        public void Write(TextWriter textWriter, LockFile lockFile)
        {
            using (var jsonWriter = new JsonTextWriter(textWriter))
            using (var jsonObjectWriter = new JsonObjectWriter(jsonWriter))
            {
                jsonWriter.Formatting = Formatting.Indented;

                WriteLockFile(jsonWriter, jsonObjectWriter, lockFile);
            }
        }

        public string Render(LockFile lockFile)
        {
            using (var writer = new StringWriter())
            {
                Write(writer, lockFile);
                return writer.ToString();
            }
        }

        private static LockFile Utf8JsonRead(Stream stream, ILogger log, string path, LockFileReadFlags flags)
        {
            try
            {
                var lockFile = JsonUtility.LoadJson(stream, Utf8JsonStreamLockFileConverters.LockFileConverter, flags);
                lockFile.Path = path;
                return lockFile;
            }
            catch (Exception ex)
            {
                log.LogError(string.Format(CultureInfo.CurrentCulture,
                    Strings.Log_ErrorReadingLockFile,
                    path, ex.Message));

                // Ran into parsing errors, mark it as unlocked and out-of-date
                return new LockFile
                {
                    Version = int.MinValue,
                    Path = path
                };
            }
        }

        private static void WriteLockFile(JsonWriter writer, IObjectWriter jsonObjectWriter, LockFile lockFile)
        {
            writer.WriteStartObject();

            writer.WritePropertyName(VersionProperty);
            writer.WriteValue(lockFile.Version);

            writer.WritePropertyName(TargetsProperty);
            JsonUtility.WriteObject(writer, lockFile.Targets, WriteTarget);

            writer.WritePropertyName(LibrariesProperty);
            JsonUtility.WriteObject(writer, lockFile.Libraries, WriteLibrary);

            writer.WritePropertyName(ProjectFileDependencyGroupsProperty);
            JsonUtility.WriteObject(writer, lockFile.ProjectFileDependencyGroups, WriteProjectFileDependencyGroup);

            if (lockFile.PackageFolders?.Any() == true)
            {
                writer.WritePropertyName(PackageFoldersProperty);
                JsonUtility.WriteObject(writer, lockFile.PackageFolders, WriteFileItem);
            }

            if (lockFile.Version >= 2)
            {
                if (lockFile.PackageSpec != null)
                {
                    writer.WritePropertyName(PackageSpecProperty);

                    jsonObjectWriter.WriteObjectStart();

                    PackageSpecWriter.Write(lockFile.PackageSpec, jsonObjectWriter, hashing: false, EnvironmentVariableWrapper.Instance, useLegacyWriter: lockFile.Version <= LegacyVersion);

                    jsonObjectWriter.WriteObjectEnd();
                }
            }

            if (lockFile.Version >= 3)
            {
                if (lockFile.LogMessages.Count > 0)
                {
                    var projectPath = lockFile.PackageSpec?.RestoreMetadata?.ProjectPath;
                    writer.WritePropertyName(LogsProperty);
                    WriteLogMessages(writer, lockFile.LogMessages, projectPath);
                }
            }

            if (lockFile.CentralTransitiveDependencyGroups.Any())
            {
                writer.WritePropertyName(CentralTransitiveDependencyGroupsProperty);
                WriteCentralTransitiveDependencyGroup(jsonObjectWriter, lockFile.CentralTransitiveDependencyGroups);
            }

            writer.WriteEndObject();
        }

        private static void WriteLibrary(JsonWriter writer, LockFileLibrary library)
        {
            writer.WritePropertyName(library.Name + "/" + library.Version.ToNormalizedString());

            writer.WriteStartObject();

            if (library.IsServiceable)
            {
                writer.WritePropertyName(ServicableProperty);
                writer.WriteValue(library.IsServiceable);
            }

            if (library.Sha512 != null)
            {
                writer.WritePropertyName(Sha512Property);
                writer.WriteValue(library.Sha512);
            }

            writer.WritePropertyName(TypeProperty);
            writer.WriteValue(library.Type);

            if (library.Path != null)
            {
                writer.WritePropertyName(PathProperty);
                writer.WriteValue(library.Path);
            }

            if (library.MSBuildProject != null)
            {
                writer.WritePropertyName(MSBuildProjectProperty);
                writer.WriteValue(library.MSBuildProject);
            }

            if (library.HasTools)
            {
                writer.WritePropertyName(HasToolsProperty);
                writer.WriteValue(library.HasTools);
            }

            WritePathArray(writer, FilesProperty, library.Files);

            writer.WriteEndObject();
        }

        private static void WriteTarget(JsonWriter writer, LockFileTarget target)
        {
            var key = target.Name;

            writer.WritePropertyName(key);

            JsonUtility.WriteObject(writer, target.Libraries, WriteTargetLibrary);
        }

        /// <summary>
        /// Writes the <see cref="IAssetsLogMessage"/> object to the <see cref="JsonWriter"/>.
        /// </summary>
        /// <param name="logMessage"><code>IAssetsLogMessage</code> representing the log message.</param>
        private static void WriteLogMessage(JsonWriter writer, IAssetsLogMessage logMessage, string projectPath)
        {
            writer.WriteStartObject();

            writer.WritePropertyName(LogMessageProperties.CODE);
            writer.WriteValue(Enum.GetName(typeof(NuGetLogCode), logMessage.Code));

            writer.WritePropertyName(LogMessageProperties.LEVEL);
            writer.WriteValue(Enum.GetName(typeof(LogLevel), logMessage.Level));

            if (logMessage.Level == LogLevel.Warning)
            {
                writer.WritePropertyName(LogMessageProperties.WARNING_LEVEL);
                writer.WriteValue((int)logMessage.WarningLevel);
            }

            if (logMessage.FilePath != null &&
               (projectPath == null || !PathUtility.GetStringComparerBasedOnOS().Equals(logMessage.FilePath, projectPath)))
            {
                // Do not write the file path if it is the same as the project path.
                // This prevents duplicate information in the lock file.
                writer.WritePropertyName(LogMessageProperties.FILE_PATH);
                writer.WriteValue(logMessage.FilePath);
            }

            if (logMessage.StartLineNumber > 0)
            {
                writer.WritePropertyName(LogMessageProperties.START_LINE_NUMBER);
                writer.WriteValue(logMessage.StartLineNumber);
            }

            if (logMessage.StartColumnNumber > 0)
            {
                writer.WritePropertyName(LogMessageProperties.START_COLUMN_NUMBER);
                writer.WriteValue(logMessage.StartColumnNumber);
            }

            if (logMessage.EndLineNumber > 0)
            {
                writer.WritePropertyName(LogMessageProperties.END_LINE_NUMBER);
                writer.WriteValue(logMessage.EndLineNumber);
            }

            if (logMessage.EndColumnNumber > 0)
            {
                writer.WritePropertyName(LogMessageProperties.END_COLUMN_NUMBER);
                writer.WriteValue(logMessage.EndColumnNumber);
            }

            if (logMessage.Message != null)
            {
                writer.WritePropertyName(LogMessageProperties.MESSAGE);
                writer.WriteValue(logMessage.Message);
            }

            if (logMessage.LibraryId != null)
            {
                writer.WritePropertyName(LogMessageProperties.LIBRARY_ID);
                writer.WriteValue(logMessage.LibraryId);
            }

            if (logMessage.TargetGraphs != null &&
                logMessage.TargetGraphs.Any() &&
                logMessage.TargetGraphs.All(l => !string.IsNullOrEmpty(l)))
            {
                writer.WritePropertyName(LogMessageProperties.TARGET_GRAPHS);
                WriteArray(writer, logMessage.TargetGraphs);
            }

            writer.WriteEndObject();
        }

        internal static void WriteLogMessages(JsonWriter writer, IEnumerable<IAssetsLogMessage> logMessages, string projectPath)
        {
            writer.WriteStartArray();

            foreach (var logMessage in logMessages)
            {
                WriteLogMessage(writer, logMessage, projectPath);
            }

            writer.WriteEndArray();
        }

        private static void WriteTargetLibrary(JsonWriter writer, LockFileTargetLibrary library)
        {
            writer.WritePropertyName(library.Name + "/" + library.Version.ToNormalizedString());

            writer.WriteStartObject();

            if (library.Type != null)
            {
                writer.WritePropertyName(TypeProperty);
                writer.WriteValue(library.Type);
            }

            if (library.Framework != null)
            {
                writer.WritePropertyName(FrameworkProperty);
                writer.WriteValue(library.Framework);
            }

            if (library.Dependencies.Count > 0)
            {
                var ordered = library.Dependencies.OrderBy(dependency => dependency.Id, StringComparer.Ordinal);

                writer.WritePropertyName(DependenciesProperty);
                JsonUtility.WriteObject(writer, ordered, JsonUtility.WritePackageDependencyWithLegacyString);
            }

            if (library.FrameworkAssemblies.Count > 0)
            {
                var ordered = library.FrameworkAssemblies.OrderBy(assembly => assembly, StringComparer.Ordinal);

                writer.WritePropertyName(FrameworkAssembliesProperty);
                WriteArray(writer, ordered);
            }

            if (library.CompileTimeAssemblies.Count > 0)
            {
                var ordered = library.CompileTimeAssemblies.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(CompileProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.RuntimeAssemblies.Count > 0)
            {
                var ordered = library.RuntimeAssemblies.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(RuntimeProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.FrameworkReferences.Count > 0)
            {
                var ordered = library.FrameworkReferences.OrderBy(reference => reference, StringComparer.Ordinal);

                writer.WritePropertyName(FrameworkReferencesProperty);
                WriteArray(writer, ordered);
            }

            if (library.ResourceAssemblies.Count > 0)
            {
                var ordered = library.ResourceAssemblies.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(ResourceProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.NativeLibraries.Count > 0)
            {
                var ordered = library.NativeLibraries.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(NativeProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.ContentFiles.Count > 0)
            {
                var ordered = library.ContentFiles.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(ContentFilesProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.Build.Count > 0)
            {
                var ordered = library.Build.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(BuildProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.BuildMultiTargeting.Count > 0)
            {
                var ordered = library.BuildMultiTargeting.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(BuildMultiTargetingProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.RuntimeTargets.Count > 0)
            {
                var ordered = library.RuntimeTargets.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(RuntimeTargetsProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.ToolsAssemblies.Count > 0)
            {
                var ordered = library.ToolsAssemblies.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(ToolsProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            if (library.EmbedAssemblies.Count > 0)
            {
                var ordered = library.EmbedAssemblies.OrderBy(assembly => assembly.Path, StringComparer.Ordinal);

                writer.WritePropertyName(EmbedProperty);
                JsonUtility.WriteObject(writer, ordered, WriteFileItem);
            }

            writer.WriteEndObject();
        }

        private static void WriteProjectFileDependencyGroup(JsonWriter writer, ProjectFileDependencyGroup frameworkInfo)
        {
            writer.WritePropertyName(frameworkInfo.FrameworkName);
            WriteArray(writer, frameworkInfo.Dependencies);
        }

        private static void WriteFileItem(JsonWriter writer, LockFileItem item)
        {
            writer.WritePropertyName(item.Path);

            writer.WriteStartObject();

            foreach (var property in item.Properties.OrderBy(x => x.Key, StringComparer.Ordinal))
            {
                writer.WritePropertyName(property.Key);

                if (bool.TrueString.Equals(property.Value, StringComparison.OrdinalIgnoreCase))
                {
                    writer.WriteValue(true);
                }
                else if (bool.FalseString.Equals(property.Value, StringComparison.OrdinalIgnoreCase))
                {
                    writer.WriteValue(false);
                }
                else
                {
                    writer.WriteValue(property.Value);
                }
            }

            writer.WriteEndObject();
        }

        private static void WritePathArray(JsonWriter writer, string property, IEnumerable<string> items)
        {
            using var itemsEnumerator = items.NoAllocEnumerate().GetEnumerator();
            if (itemsEnumerator.MoveNext())
            {
                var orderedItems = items
                    .Select(f => GetPathWithForwardSlashes(f))
                    .OrderBy(f => f, StringComparer.Ordinal);

                writer.WritePropertyName(property);
                WriteArray(writer, orderedItems);
            }
        }

        internal static void WriteArray(JsonWriter writer, IEnumerable<string> values)
        {
            writer.WriteStartArray();
            foreach (var value in values)
            {
                writer.WriteValue(value);
            }
            writer.WriteEndArray();
        }

        private static string GetPathWithForwardSlashes(string path)
        {
            return path.Replace('\\', '/');
        }

        private static void WriteCentralTransitiveDependencyGroup(IObjectWriter writer, IList<CentralTransitiveDependencyGroup> centralTransitiveDependencyGroups)
        {
            writer.WriteObjectStart();

            foreach (var centralTransitiveDepGroup in centralTransitiveDependencyGroups.OrderBy(ptdg => ptdg.FrameworkName))
            {
                PackageSpecWriter.SetCentralTransitveDependencyGroup(writer, centralTransitiveDepGroup.FrameworkName, centralTransitiveDepGroup.TransitiveDependencies);
            }

            writer.WriteObjectEnd();
        }
    }
}