work work hard work
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph;
|
||||
|
||||
public sealed class ReachabilityAnalyzer
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly int _maxDepth;
|
||||
|
||||
public ReachabilityAnalyzer(TimeProvider? timeProvider = null, int maxDepth = 256)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_maxDepth = maxDepth <= 0 ? 256 : maxDepth;
|
||||
}
|
||||
|
||||
public ReachabilityAnalysisResult Analyze(CallGraphSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
var trimmed = snapshot.Trimmed();
|
||||
|
||||
var adjacency = BuildAdjacency(trimmed);
|
||||
|
||||
var entrypoints = trimmed.EntrypointIds;
|
||||
if (entrypoints.IsDefaultOrEmpty)
|
||||
{
|
||||
return EmptyResult(trimmed);
|
||||
}
|
||||
|
||||
var origins = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var parents = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
var depths = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var queue = new Queue<string>();
|
||||
|
||||
foreach (var entry in entrypoints.OrderBy(e => e, StringComparer.Ordinal))
|
||||
{
|
||||
origins[entry] = entry;
|
||||
parents[entry] = null;
|
||||
depths[entry] = 0;
|
||||
queue.Enqueue(entry);
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (!depths.TryGetValue(current, out var depth))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth >= _maxDepth)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!adjacency.TryGetValue(current, out var neighbors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var next in neighbors)
|
||||
{
|
||||
if (origins.ContainsKey(next))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
origins[next] = origins[current];
|
||||
parents[next] = current;
|
||||
depths[next] = depth + 1;
|
||||
queue.Enqueue(next);
|
||||
}
|
||||
}
|
||||
|
||||
var reachableNodes = origins.Keys.OrderBy(id => id, StringComparer.Ordinal).ToImmutableArray();
|
||||
var reachableSinks = trimmed.SinkIds
|
||||
.Where(origins.ContainsKey)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var paths = BuildPaths(reachableSinks, origins, parents);
|
||||
|
||||
var computedAt = _timeProvider.GetUtcNow();
|
||||
var provisional = new ReachabilityAnalysisResult(
|
||||
ScanId: trimmed.ScanId,
|
||||
GraphDigest: trimmed.GraphDigest,
|
||||
Language: trimmed.Language,
|
||||
ComputedAt: computedAt,
|
||||
ReachableNodeIds: reachableNodes,
|
||||
ReachableSinkIds: reachableSinks,
|
||||
Paths: paths,
|
||||
ResultDigest: string.Empty);
|
||||
|
||||
var resultDigest = CallGraphDigests.ComputeResultDigest(provisional);
|
||||
return provisional with { ResultDigest = resultDigest };
|
||||
}
|
||||
|
||||
private static Dictionary<string, ImmutableArray<string>> BuildAdjacency(CallGraphSnapshot snapshot)
|
||||
{
|
||||
var map = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
foreach (var edge in snapshot.Edges)
|
||||
{
|
||||
if (!map.TryGetValue(edge.SourceId, out var list))
|
||||
{
|
||||
list = new List<string>();
|
||||
map[edge.SourceId] = list;
|
||||
}
|
||||
list.Add(edge.TargetId);
|
||||
}
|
||||
|
||||
return map.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(v => v, StringComparer.Ordinal)
|
||||
.ToImmutableArray(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static ReachabilityAnalysisResult EmptyResult(CallGraphSnapshot snapshot)
|
||||
{
|
||||
var computedAt = TimeProvider.System.GetUtcNow();
|
||||
var provisional = new ReachabilityAnalysisResult(
|
||||
ScanId: snapshot.ScanId,
|
||||
GraphDigest: snapshot.GraphDigest,
|
||||
Language: snapshot.Language,
|
||||
ComputedAt: computedAt,
|
||||
ReachableNodeIds: ImmutableArray<string>.Empty,
|
||||
ReachableSinkIds: ImmutableArray<string>.Empty,
|
||||
Paths: ImmutableArray<ReachabilityPath>.Empty,
|
||||
ResultDigest: string.Empty);
|
||||
|
||||
return provisional with { ResultDigest = CallGraphDigests.ComputeResultDigest(provisional) };
|
||||
}
|
||||
|
||||
private static ImmutableArray<ReachabilityPath> BuildPaths(
|
||||
ImmutableArray<string> reachableSinks,
|
||||
Dictionary<string, string> origins,
|
||||
Dictionary<string, string?> parents)
|
||||
{
|
||||
var paths = new List<ReachabilityPath>(reachableSinks.Length);
|
||||
foreach (var sinkId in reachableSinks)
|
||||
{
|
||||
if (!origins.TryGetValue(sinkId, out var origin))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodeIds = ReconstructPathNodeIds(sinkId, parents);
|
||||
paths.Add(new ReachabilityPath(origin, sinkId, nodeIds));
|
||||
}
|
||||
|
||||
return paths
|
||||
.OrderBy(p => p.SinkId, StringComparer.Ordinal)
|
||||
.ThenBy(p => p.EntrypointId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ReconstructPathNodeIds(string sinkId, Dictionary<string, string?> parents)
|
||||
{
|
||||
var stack = new Stack<string>();
|
||||
var cursor = sinkId;
|
||||
while (true)
|
||||
{
|
||||
stack.Push(cursor);
|
||||
if (!parents.TryGetValue(cursor, out var parent) || parent is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
cursor = parent;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<string>(stack.Count);
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
builder.Add(stack.Pop());
|
||||
}
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user