Skip to content

Commit 7bbf1a2

Browse files
committed
C#: Add assets.json parser.
1 parent c6c00e7 commit 7bbf1a2

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using Newtonsoft.Json.Linq;
6+
7+
namespace Semmle.Extraction.CSharp.DependencyFetching
8+
{
9+
/// <summary>
10+
/// Class for parsing project.assets.json files.
11+
/// </summary>
12+
internal class Assets
13+
{
14+
private readonly ProgressMonitor progressMonitor;
15+
16+
private static readonly string[] netFrameworks = new[] {
17+
"microsoft.aspnetcore.app.ref",
18+
"microsoft.netcore.app.ref",
19+
"microsoft.netframework.referenceassemblies",
20+
"microsoft.windowsdesktop.app.ref",
21+
"netstandard.library.ref"
22+
};
23+
24+
25+
internal Assets(ProgressMonitor progressMonitor)
26+
{
27+
this.progressMonitor = progressMonitor;
28+
}
29+
30+
/// <summary>
31+
/// In most cases paths in asset files point to dll's or the empty _._ file, which
32+
/// is sometimes there to avoid the directory being empty.
33+
/// That is, if the path specifically adds a .dll we use that, otherwise we as a fallback
34+
/// add the entire directory (which should be fine in case of _._ as well).
35+
/// </summary>
36+
private static string ParseFilePath(string path)
37+
{
38+
if (path.EndsWith(".dll"))
39+
{
40+
return path;
41+
}
42+
return Path.GetDirectoryName(path) ?? path;
43+
}
44+
45+
/// <summary>
46+
/// Class needed for deserializing parts of an assets file.
47+
/// It holds information about a reference.
48+
/// </summary>
49+
private class ReferenceInfo
50+
{
51+
/// <summary>
52+
/// This carries the type of the reference.
53+
/// We are only interested in package references.
54+
/// </summary>
55+
public string Type { get; set; } = "";
56+
57+
/// <summary>
58+
/// If not a .NET framework reference we assume that only the files mentioned
59+
/// in the compile section are needed for compilation.
60+
/// </summary>
61+
public Dictionary<string, object> Compile { get; set; } = new();
62+
}
63+
64+
/// <summary>
65+
/// Gets the package dependencies from the assets file.
66+
///
67+
/// Parse a part of the JSon assets file and returns the paths
68+
/// to the dependencies required for compilation.
69+
///
70+
/// Example:
71+
/// {
72+
/// "Castle.Core/4.4.1": {
73+
/// "type": "package",
74+
/// "compile": {
75+
/// "lib/netstandard1.5/Castle.Core.dll": {
76+
/// "related": ".xml"
77+
/// }
78+
/// }
79+
/// },
80+
/// "Json.Net/1.0.33": {
81+
/// "type": "package",
82+
/// "compile": {
83+
/// "lib/netstandard2.0/Json.Net.dll": {}
84+
/// },
85+
/// "runtime": {
86+
/// "lib/netstandard2.0/Json.Net.dll": {}
87+
/// }
88+
/// }
89+
/// }
90+
///
91+
/// Returns {
92+
/// "castle.core/4.4.1/lib/netstandard1.5/Castle.Core.dll",
93+
/// "json.net/1.0.33/lib/netstandard2.0/Json.Net.dll"
94+
/// }
95+
/// </summary>
96+
private IEnumerable<string> GetPackageDependencies(JObject json)
97+
{
98+
// If there are more than one framework we need to pick just one.
99+
// To ensure stability we pick one based on the lexicographic order of
100+
// the framework names.
101+
var references = json
102+
.GetProperty("targets")?
103+
.Properties()?
104+
.MaxBy(p => p.Name)?
105+
.Value
106+
.ToObject<Dictionary<string, ReferenceInfo>>();
107+
108+
if (references is null)
109+
{
110+
progressMonitor.LogDebug("No references found in the targets section in the assets file.");
111+
return Array.Empty<string>();
112+
}
113+
114+
// Find all the compile dependencies for each reference and
115+
// create the relative path to the dependency.
116+
var dependencies = references
117+
.SelectMany(r =>
118+
{
119+
var info = r.Value;
120+
var name = r.Key.ToLowerInvariant();
121+
if (info.Type != "package")
122+
{
123+
return Array.Empty<string>();
124+
}
125+
126+
// If this is a .NET framework reference then include everything.
127+
return netFrameworks.Any(framework => name.StartsWith(framework))
128+
? new[] { name }
129+
: info
130+
.Compile
131+
.Select(p => Path.Combine(name, ParseFilePath(p.Key)));
132+
})
133+
.ToList();
134+
135+
return dependencies;
136+
}
137+
138+
/// <summary>
139+
/// Parse `json` as project.assets.json content and populate `dependencies` with the
140+
/// relative paths to the dependencies required for compilation.
141+
/// </summary>
142+
/// <returns>True if parsing succeeds, otherwise false.</returns>
143+
public bool TryParse(string json, out IEnumerable<string> dependencies)
144+
{
145+
dependencies = Array.Empty<string>();
146+
147+
try
148+
{
149+
var obj = JObject.Parse(json);
150+
var packages = GetPackageDependencies(obj);
151+
152+
dependencies = packages.ToList();
153+
154+
return true;
155+
}
156+
catch (Exception e)
157+
{
158+
progressMonitor.LogDebug($"Failed to parse assets file (unexpected error): {e.Message}");
159+
return false;
160+
}
161+
}
162+
}
163+
164+
internal static class JsonExtensions
165+
{
166+
internal static JObject? GetProperty(this JObject json, string property) =>
167+
json[property] as JObject;
168+
}
169+
}

csharp/extractor/Semmle.Util/IEnumerableExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,11 @@ public static int SequenceHash<T>(this IEnumerable<T> items) where T : notnull
113113
h = h * 7 + i.GetHashCode();
114114
return h;
115115
}
116+
117+
/// <summary>
118+
/// Returns the sequence with nulls removed.
119+
/// </summary>
120+
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> items) where T : class =>
121+
items.Where(i => i is not null)!;
116122
}
117123
}

0 commit comments

Comments
 (0)