using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using Semmle.Util; using Semmle.Util.Logging; namespace Semmle.Extraction.CSharp.DependencyFetching { /// /// Class for parsing project.assets.json files. /// internal class Assets { private readonly ILogger logger; /// /// Contains the dependencies found in the parsed assets files. /// public DependencyContainer Dependencies { get; } = new(); internal Assets(ILogger logger) { this.logger = logger; } /// /// Class needed for deserializing parts of an assets file. /// It holds information about a reference. /// /// Type carries the type of the reference. /// We are only interested in package references. /// /// Compile holds information about the files needed for compilation. /// However, if it is a .NET framework reference we assume that all files in the /// package are needed for compilation. /// private record class ReferenceInfo(string? Type, Dictionary? Compile); /// /// Add the package dependencies from the assets file to dependencies. /// /// Parse a part of the JSon assets file and add the paths /// to the dependencies required for compilation (and collect /// information about used packages). /// /// Example: /// { /// "Castle.Core/4.4.1": { /// "type": "package", /// "compile": { /// "lib/netstandard1.5/Castle.Core.dll": { /// "related": ".xml" /// } /// } /// }, /// "Json.Net/1.0.33": { /// "type": "package", /// "compile": { /// "lib/netstandard2.0/Json.Net.dll": {} /// }, /// "runtime": { /// "lib/netstandard2.0/Json.Net.dll": {} /// } /// } /// } /// /// Adds the following dependencies /// Paths: { /// "castle.core/4.4.1/lib/netstandard1.5/Castle.Core.dll", /// "json.net/1.0.33/lib/netstandard2.0/Json.Net.dll" /// } /// Packages: { /// "castle.core", /// "json.net" /// } /// private void AddPackageDependencies(JObject json, string jsonPath) { // If there is more than one framework we need to pick just one. // To ensure stability we pick one based on the lexicographic order of // the framework names. var references = json .GetProperty("targets")? .Properties()? .MaxBy(p => p.Name)? .Value .ToObject>(); if (references is null) { logger.LogDebug($"No references found in the targets section in '{jsonPath}'"); return; } // Find all the compile dependencies for each reference and // create the relative path to the dependency. references .ForEach(r => { var info = r.Value; var name = r.Key.ToLowerInvariant(); if (info.Type != "package") { return; } if (info.Compile is null || !info.Compile.Any()) { // If this is a framework reference then include everything. if (FrameworkPackageNames.AllFrameworks.Any(framework => name.StartsWith(framework))) { Dependencies.AddFramework(name); } return; } info.Compile .ForEach(r => Dependencies.Add(name, r.Key)); }); } /// /// Add the framework dependencies from the assets file to dependencies. /// /// Example: /// "project": { /// "version": "1.0.0", /// "frameworks": { /// "net7.0": { /// "frameworkReferences": { /// "Microsoft.AspNetCore.App": { /// "privateAssets": "none" /// }, /// "Microsoft.NETCore.App": { /// "privateAssets": "all" /// } /// } /// } /// } /// } /// /// Adds the following dependencies /// Paths: { /// "microsoft.aspnetcore.app.ref", /// "microsoft.netcore.app.ref" /// } /// Packages: { /// "microsoft.aspnetcore.app.ref", /// "microsoft.netcore.app.ref" /// } /// private void AddFrameworkDependencies(JObject json, string jsonPath) { var frameworks = json .GetProperty("project")? .GetProperty("frameworks"); if (frameworks is null) { logger.LogDebug($"No framework section in '{jsonPath}'."); return; } // If there is more than one framework we need to pick just one. // To ensure stability we pick one based on the lexicographic order of // the framework names. var references = frameworks .Properties()? .MaxBy(p => p.Name)? .Value["frameworkReferences"] as JObject; if (references is null) { logger.LogDebug($"No framework references in '{jsonPath}'."); return; } references .Properties() .ForEach(f => Dependencies.AddFramework($"{f.Name}.Ref".ToLowerInvariant())); } /// /// Parse `json` as project.assets.json content and add relative paths to the dependencies /// (together with used package information) required for compilation. /// /// True if parsing succeeds, otherwise false. public bool TryParse(string json, string jsonPath) { try { var obj = JObject.Parse(json); AddPackageDependencies(obj, jsonPath); AddFrameworkDependencies(obj, jsonPath); return true; } catch (Exception e) { logger.LogDebug($"Failed to parse assets file (unexpected error): {e.Message}"); return false; } } private static bool TryReadAllText(string path, ILogger logger, [NotNullWhen(returnValue: true)] out string? content) { try { content = File.ReadAllText(path); return true; } catch (Exception e) { logger.LogInfo($"Failed to read assets file '{path}': {e.Message}"); content = null; return false; } } /// /// Add the dependencies from the assets file to the dependencies. /// /// Path to an assets file. public void AddDependencies(string asset) { if (TryReadAllText(asset, logger, out var json)) { TryParse(json, asset); } } /// /// Add the dependencies from the assets files to the dependencies. /// /// Collection of paths to assets files. public void AddDependenciesRange(IEnumerable assets) => assets.ForEach(AddDependencies); } internal static class JsonExtensions { internal static JObject? GetProperty(this JObject json, string property) => json[property] as JObject; } }