|
|
|
@ -137,6 +137,9 @@ var downloadOnly = args.Contains("--download-only"); |
|
|
|
var pdbMode = args.Contains("--pdb"); |
|
|
|
var pdbMode = args.Contains("--pdb"); |
|
|
|
// Checks the PDB the assembly already ships with. Calibration, not a sweep.
|
|
|
|
// Checks the PDB the assembly already ships with. Calibration, not a sweep.
|
|
|
|
var pdbLint = args.Contains("--pdb-lint"); |
|
|
|
var pdbLint = args.Contains("--pdb-lint"); |
|
|
|
|
|
|
|
// Findings first hit by the assembly being decompiled. They get their reference context
|
|
|
|
|
|
|
|
// once it is done, because the resolver keeps discovering references until then.
|
|
|
|
|
|
|
|
var pendingContext = new List<string>(); |
|
|
|
var arguments = args |
|
|
|
var arguments = args |
|
|
|
.Where(a => !a.StartsWith("--")) |
|
|
|
.Where(a => !a.StartsWith("--")) |
|
|
|
.SelectMany(a => a.StartsWith('@') ? File.ReadAllLines(a[1..]) : new[] { a }) |
|
|
|
.SelectMany(a => a.StartsWith('@') ? File.ReadAllLines(a[1..]) : new[] { a }) |
|
|
|
@ -554,6 +557,10 @@ async Task<string> GetPackage(string id, NuGetVersion version) |
|
|
|
|
|
|
|
|
|
|
|
async Task DecompileAssembly(string pkg, string dllPath, List<string> searchDirs, NuGetFramework matchTarget, string? fallbackDir) |
|
|
|
async Task DecompileAssembly(string pkg, string dllPath, List<string> searchDirs, NuGetFramework matchTarget, string? fallbackDir) |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
// An assembly that bails out before reporting its resolutions leaves findings behind that
|
|
|
|
|
|
|
|
// never received a context. Dropping them here keeps the next assembly from stamping its
|
|
|
|
|
|
|
|
// own references onto them, which would name the wrong reference set for the finding.
|
|
|
|
|
|
|
|
pendingContext.Clear(); |
|
|
|
var name = Path.GetFileName(dllPath); |
|
|
|
var name = Path.GetFileName(dllPath); |
|
|
|
PEFile module; |
|
|
|
PEFile module; |
|
|
|
try |
|
|
|
try |
|
|
|
@ -567,6 +574,9 @@ async Task DecompileAssembly(string pkg, string dllPath, List<string> searchDirs |
|
|
|
} |
|
|
|
} |
|
|
|
using (module) |
|
|
|
using (module) |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
// The file name alone is ambiguous across a sweep: the same simple name ships in many
|
|
|
|
|
|
|
|
// packages and many TFM folders. Identify findings by full assembly name plus path.
|
|
|
|
|
|
|
|
name = $"{module.FullName} ({dllPath})"; |
|
|
|
// ".NETCoreApp,Version=v5.0" -> 5.0; null for .NET Framework / netstandard modules.
|
|
|
|
// ".NETCoreApp,Version=v5.0" -> 5.0; null for .NET Framework / netstandard modules.
|
|
|
|
Version? coreVersion = null; |
|
|
|
Version? coreVersion = null; |
|
|
|
var tfmId = module.DetectTargetFrameworkId(); |
|
|
|
var tfmId = module.DetectTargetFrameworkId(); |
|
|
|
@ -620,7 +630,7 @@ async Task DecompileAssembly(string pkg, string dllPath, List<string> searchDirs |
|
|
|
if (pdbMode) |
|
|
|
if (pdbMode) |
|
|
|
{ |
|
|
|
{ |
|
|
|
CheckGeneratedPdb(pkg, name, module, decompiler, dllPath, orderedDirs); |
|
|
|
CheckGeneratedPdb(pkg, name, module, decompiler, dllPath, orderedDirs); |
|
|
|
ReportResolutions(logResolver); |
|
|
|
ReportResolutions(logResolver, dllPath, orderedDirs); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
foreach (var type in decompiler.TypeSystem.MainModule.TopLevelTypeDefinitions.ToList()) |
|
|
|
foreach (var type in decompiler.TypeSystem.MainModule.TopLevelTypeDefinitions.ToList()) |
|
|
|
@ -674,7 +684,7 @@ async Task DecompileAssembly(string pkg, string dllPath, List<string> searchDirs |
|
|
|
Report(pkg, name, type.FullTypeName.ToString(), ex); |
|
|
|
Report(pkg, name, type.FullTypeName.ToString(), ex); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
ReportResolutions(logResolver); |
|
|
|
ReportResolutions(logResolver, dllPath, orderedDirs); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -685,10 +695,21 @@ string SyntaxTreeToString(SyntaxTree syntaxTree) |
|
|
|
return w.ToString(); |
|
|
|
return w.ToString(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void ReportResolutions(LoggingResolver logResolver) |
|
|
|
void ReportResolutions(LoggingResolver logResolver, string dllPath, List<string> orderedDirs) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var resolutions = logResolver.Resolutions; |
|
|
|
var resolutions = logResolver.Resolutions; |
|
|
|
var unresolved = resolutions.Where(kv => kv.Value == null).Select(kv => kv.Key).OrderBy(k => k).ToList(); |
|
|
|
var unresolved = resolutions.Where(kv => kv.Value == null).Select(kv => kv.Key).OrderBy(k => k).ToList(); |
|
|
|
|
|
|
|
// What it takes to reproduce a finding by hand: the assembly it came from and the
|
|
|
|
|
|
|
|
// references it was decompiled against, which are what the report is read for once
|
|
|
|
|
|
|
|
// a warning turns out to be a reference problem rather than a decompiler defect.
|
|
|
|
|
|
|
|
var context = new StringBuilder($"assembly: {dllPath}"); |
|
|
|
|
|
|
|
foreach (var dir in orderedDirs) |
|
|
|
|
|
|
|
context.Append($"{Environment.NewLine} -r {dir}"); |
|
|
|
|
|
|
|
foreach (var (refName, refPath) in resolutions.OrderBy(kv => kv.Key, StringComparer.Ordinal)) |
|
|
|
|
|
|
|
context.Append($"{Environment.NewLine} {refName} -> {refPath ?? "NOT FOUND"}"); |
|
|
|
|
|
|
|
foreach (var key in pendingContext) |
|
|
|
|
|
|
|
failures[key] = failures[key] with { Context = context.ToString() }; |
|
|
|
|
|
|
|
pendingContext.Clear(); |
|
|
|
refsTotal += resolutions.Count; |
|
|
|
refsTotal += resolutions.Count; |
|
|
|
refsResolved += resolutions.Count - unresolved.Count; |
|
|
|
refsResolved += resolutions.Count - unresolved.Count; |
|
|
|
Console.WriteLine($" refs: {resolutions.Count - unresolved.Count}/{resolutions.Count} resolved"); |
|
|
|
Console.WriteLine($" refs: {resolutions.Count - unresolved.Count}/{resolutions.Count} resolved"); |
|
|
|
@ -768,7 +789,6 @@ void CheckGeneratedPdb(string pkg, string asm, PEFile module, CSharpDecompiler d |
|
|
|
// reported here is a defect in the lint rather than in ILSpy.
|
|
|
|
// reported here is a defect in the lint rather than in ILSpy.
|
|
|
|
void LintExistingPdb(string dllPath) |
|
|
|
void LintExistingPdb(string dllPath) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var asm = Path.GetFileName(dllPath); |
|
|
|
|
|
|
|
using var peStream = File.OpenRead(dllPath); |
|
|
|
using var peStream = File.OpenRead(dllPath); |
|
|
|
PEReader peReader; |
|
|
|
PEReader peReader; |
|
|
|
try |
|
|
|
try |
|
|
|
@ -783,6 +803,7 @@ void LintExistingPdb(string dllPath) |
|
|
|
} |
|
|
|
} |
|
|
|
using (peReader) |
|
|
|
using (peReader) |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
var asm = $"{peReader.GetMetadataReader().GetFullAssemblyName()} ({dllPath})"; |
|
|
|
// A PDB next to the assembly if there is one, otherwise the one embedded in the PE.
|
|
|
|
// A PDB next to the assembly if there is one, otherwise the one embedded in the PE.
|
|
|
|
MemoryStream? pdbStream = null; |
|
|
|
MemoryStream? pdbStream = null; |
|
|
|
MetadataReaderProvider? provider = null; |
|
|
|
MetadataReaderProvider? provider = null; |
|
|
|
@ -1304,6 +1325,7 @@ void Report(string pkg, string asm, string type, Exception ex) |
|
|
|
{ |
|
|
|
{ |
|
|
|
failures[key] = new Finding(kind, inner.GetType().Name, FirstLine(inner.Message), |
|
|
|
failures[key] = new Finding(kind, inner.GetType().Name, FirstLine(inner.Message), |
|
|
|
FirstLine(topFrame), location, ex.ToString(), 1); |
|
|
|
FirstLine(topFrame), location, ex.ToString(), 1); |
|
|
|
|
|
|
|
pendingContext.Add(key); |
|
|
|
Console.WriteLine($" [{kind}] {location}"); |
|
|
|
Console.WriteLine($" [{kind}] {location}"); |
|
|
|
foreach (var line in ex.ToString().Split('\n').Take(30)) |
|
|
|
foreach (var line in ex.ToString().Split('\n').Take(30)) |
|
|
|
Console.WriteLine(" " + line.TrimEnd()); |
|
|
|
Console.WriteLine(" " + line.TrimEnd()); |
|
|
|
@ -1324,7 +1346,7 @@ static void AppendToLedger(string path, IEnumerable<Finding> findings, int assem |
|
|
|
// often than a decompiler defect ("might be due to ... missing references" is what
|
|
|
|
// often than a decompiler defect ("might be due to ... missing references" is what
|
|
|
|
// the warning itself says), and the report separates the two on this basis.
|
|
|
|
// the warning itself says), and the report separates the two on this basis.
|
|
|
|
lines.Add(JsonSerializer.Serialize(new LedgerEntry("finding", f.Kind, f.ExceptionType, f.Message, |
|
|
|
lines.Add(JsonSerializer.Serialize(new LedgerEntry("finding", f.Kind, f.ExceptionType, f.Message, |
|
|
|
f.Frame, f.FirstLocation, f.Detail, f.Count, 0, 0, refsResolved, refsTotal))); |
|
|
|
f.Frame, f.FirstLocation, f.Detail, f.Count, 0, 0, refsResolved, refsTotal, f.Context))); |
|
|
|
} |
|
|
|
} |
|
|
|
lines.Add(JsonSerializer.Serialize(new LedgerEntry("totals", "", "", "", "", "", "", 0, |
|
|
|
lines.Add(JsonSerializer.Serialize(new LedgerEntry("totals", "", "", "", "", "", "", 0, |
|
|
|
assemblies, types, refsResolved, refsTotal))); |
|
|
|
assemblies, types, refsResolved, refsTotal))); |
|
|
|
@ -1385,7 +1407,7 @@ static void RenderLedger(string ledgerPath, string outPath) |
|
|
|
merged[key] = merged.TryGetValue(key, out var existing) |
|
|
|
merged[key] = merged.TryGetValue(key, out var existing) |
|
|
|
? existing with { Count = existing.Count + entry.Count } |
|
|
|
? existing with { Count = existing.Count + entry.Count } |
|
|
|
: new Finding(entry.Kind, entry.ExceptionType, entry.Message, entry.Frame, |
|
|
|
: new Finding(entry.Kind, entry.ExceptionType, entry.Message, entry.Frame, |
|
|
|
entry.FirstLocation, entry.Detail, entry.Count); |
|
|
|
entry.FirstLocation, entry.Detail, entry.Count, entry.Context); |
|
|
|
// Ledger lines written before this attribution existed carry 0/0; treat those as
|
|
|
|
// Ledger lines written before this attribution existed carry 0/0; treat those as
|
|
|
|
// unknown rather than clean, so they are never presented as confirmed defects.
|
|
|
|
// unknown rather than clean, so they are never presented as confirmed defects.
|
|
|
|
(entry.RefsTotal > 0 && entry.RefsResolved == entry.RefsTotal ? clean : degraded).Add(key); |
|
|
|
(entry.RefsTotal > 0 && entry.RefsResolved == entry.RefsTotal ? clean : degraded).Add(key); |
|
|
|
@ -1450,7 +1472,8 @@ static void WriteHtmlReport(string path, List<Finding> findings, int assemblies, |
|
|
|
: ""; |
|
|
|
: ""; |
|
|
|
html.AppendLine($"<details class={kind}><summary><span class=count>{f.Count}x</span> " |
|
|
|
html.AppendLine($"<details class={kind}><summary><span class=count>{f.Count}x</span> " |
|
|
|
+ $"{Esc(f.ExceptionType)}: {Esc(f.Message)}{suspect}</summary>"); |
|
|
|
+ $"{Esc(f.ExceptionType)}: {Esc(f.Message)}{suspect}</summary>"); |
|
|
|
html.AppendLine($"<pre>first: {Esc(f.FirstLocation)}\nframe: {Esc(f.Frame)}\n\n{Esc(f.Detail)}</pre></details>"); |
|
|
|
var context = f.Context.Length > 0 ? $"{Esc(f.Context)}\n" : ""; |
|
|
|
|
|
|
|
html.AppendLine($"<pre>first: {Esc(f.FirstLocation)}\nframe: {Esc(f.Frame)}\n{context}\n{Esc(f.Detail)}</pre></details>"); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
html.AppendLine("""
|
|
|
|
html.AppendLine("""
|
|
|
|
@ -1479,7 +1502,7 @@ static string FirstLine(string s) |
|
|
|
// One deduplicated defect: Count counts every location that hit it, Detail keeps the
|
|
|
|
// One deduplicated defect: Count counts every location that hit it, Detail keeps the
|
|
|
|
// full exception text of the first one for triage.
|
|
|
|
// full exception text of the first one for triage.
|
|
|
|
record Finding(string Kind, string ExceptionType, string Message, string Frame, |
|
|
|
record Finding(string Kind, string ExceptionType, string Message, string Frame, |
|
|
|
string FirstLocation, string Detail, int Count) |
|
|
|
string FirstLocation, string Detail, int Count, string Context = "") |
|
|
|
{ |
|
|
|
{ |
|
|
|
public string Describe() |
|
|
|
public string Describe() |
|
|
|
=> $"[{Kind}] {ExceptionType}: {Message} @ {Frame} (first: {FirstLocation})"; |
|
|
|
=> $"[{Kind}] {ExceptionType}: {Message} @ {Frame} (first: {FirstLocation})"; |
|
|
|
@ -1488,7 +1511,7 @@ record Finding(string Kind, string ExceptionType, string Message, string Frame, |
|
|
|
// One line of the sweep ledger: either a deduplicated finding or a per-run totals record.
|
|
|
|
// One line of the sweep ledger: either a deduplicated finding or a per-run totals record.
|
|
|
|
record LedgerEntry(string Record, string Kind, string ExceptionType, string Message, string Frame, |
|
|
|
record LedgerEntry(string Record, string Kind, string ExceptionType, string Message, string Frame, |
|
|
|
string FirstLocation, string Detail, int Count, int Assemblies, int Types, |
|
|
|
string FirstLocation, string Detail, int Count, int Assemblies, int Types, |
|
|
|
long RefsResolved, long RefsTotal); |
|
|
|
long RefsResolved, long RefsTotal, string Context = ""); |
|
|
|
|
|
|
|
|
|
|
|
record VersionIndex(string[] versions); |
|
|
|
record VersionIndex(string[] versions); |
|
|
|
|
|
|
|
|
|
|
|
|