Add Canonical JSON serialization library with tests and documentation
- Implemented CanonJson class for deterministic JSON serialization and hashing. - Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters. - Created project files for the Canonical JSON library and its tests, including necessary package references. - Added README.md for library usage and API reference. - Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
@@ -2,20 +2,53 @@ using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes call graph reachability from entrypoints to sinks using BFS traversal.
|
||||
/// Provides deterministically-ordered paths suitable for witness generation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sprint: SPRINT_3700_0001_0001 (WIT-007A, WIT-007B)
|
||||
/// Contract: Paths are ordered by (SinkId ASC, EntrypointId ASC, PathLength ASC).
|
||||
/// Node IDs within paths are ordered from entrypoint to sink (caller → callee).
|
||||
/// </remarks>
|
||||
public sealed class ReachabilityAnalyzer
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly int _maxDepth;
|
||||
private readonly ReachabilityAnalysisOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ReachabilityAnalyzer with default options.
|
||||
/// </summary>
|
||||
public ReachabilityAnalyzer(TimeProvider? timeProvider = null, int maxDepth = 256)
|
||||
: this(timeProvider, new ReachabilityAnalysisOptions { MaxDepth = maxDepth })
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_maxDepth = maxDepth <= 0 ? 256 : maxDepth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ReachabilityAnalyzer with specified options.
|
||||
/// </summary>
|
||||
public ReachabilityAnalyzer(TimeProvider? timeProvider, ReachabilityAnalysisOptions options)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_options = (options ?? ReachabilityAnalysisOptions.Default).Validated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes reachability using default options.
|
||||
/// </summary>
|
||||
public ReachabilityAnalysisResult Analyze(CallGraphSnapshot snapshot)
|
||||
=> Analyze(snapshot, _options);
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes reachability with explicit options for this invocation.
|
||||
/// </summary>
|
||||
/// <param name="snapshot">The call graph snapshot to analyze.</param>
|
||||
/// <param name="options">Options controlling limits and output format.</param>
|
||||
/// <returns>Analysis result with deterministically-ordered paths.</returns>
|
||||
public ReachabilityAnalysisResult Analyze(CallGraphSnapshot snapshot, ReachabilityAnalysisOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
var opts = (options ?? _options).Validated();
|
||||
var trimmed = snapshot.Trimmed();
|
||||
|
||||
var adjacency = BuildAdjacency(trimmed);
|
||||
@@ -47,7 +80,7 @@ public sealed class ReachabilityAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth >= _maxDepth)
|
||||
if (depth >= opts.MaxDepth)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -72,12 +105,18 @@ public sealed class ReachabilityAnalyzer
|
||||
}
|
||||
|
||||
var reachableNodes = origins.Keys.OrderBy(id => id, StringComparer.Ordinal).ToImmutableArray();
|
||||
var reachableSinks = trimmed.SinkIds
|
||||
|
||||
// WIT-007B: Use explicit sinks if specified, otherwise use snapshot sinks
|
||||
var targetSinks = opts.ExplicitSinks.HasValue && !opts.ExplicitSinks.Value.IsDefaultOrEmpty
|
||||
? opts.ExplicitSinks.Value
|
||||
: trimmed.SinkIds;
|
||||
|
||||
var reachableSinks = targetSinks
|
||||
.Where(origins.ContainsKey)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var paths = BuildPaths(reachableSinks, origins, parents);
|
||||
var paths = BuildPaths(reachableSinks, origins, parents, opts);
|
||||
|
||||
var computedAt = _timeProvider.GetUtcNow();
|
||||
var provisional = new ReachabilityAnalysisResult(
|
||||
@@ -136,9 +175,12 @@ public sealed class ReachabilityAnalyzer
|
||||
private static ImmutableArray<ReachabilityPath> BuildPaths(
|
||||
ImmutableArray<string> reachableSinks,
|
||||
Dictionary<string, string> origins,
|
||||
Dictionary<string, string?> parents)
|
||||
Dictionary<string, string?> parents,
|
||||
ReachabilityAnalysisOptions options)
|
||||
{
|
||||
var paths = new List<ReachabilityPath>(reachableSinks.Length);
|
||||
var pathCountPerSink = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var sinkId in reachableSinks)
|
||||
{
|
||||
if (!origins.TryGetValue(sinkId, out var origin))
|
||||
@@ -146,13 +188,29 @@ public sealed class ReachabilityAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enforce per-sink limit
|
||||
pathCountPerSink.TryGetValue(sinkId, out var currentCount);
|
||||
if (currentCount >= options.MaxPathsPerSink)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
pathCountPerSink[sinkId] = currentCount + 1;
|
||||
|
||||
var nodeIds = ReconstructPathNodeIds(sinkId, parents);
|
||||
paths.Add(new ReachabilityPath(origin, sinkId, nodeIds));
|
||||
|
||||
// Enforce total path limit
|
||||
if (paths.Count >= options.MaxTotalPaths)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic ordering: SinkId ASC, EntrypointId ASC, PathLength ASC
|
||||
return paths
|
||||
.OrderBy(p => p.SinkId, StringComparer.Ordinal)
|
||||
.ThenBy(p => p.EntrypointId, StringComparer.Ordinal)
|
||||
.ThenBy(p => p.NodeIds.Length)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user