audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AbiCompatibility.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Task: SYM-005 - Define AbiCompatibility assessment model
|
||||
// Description: ABI compatibility assessment model
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// ABI compatibility assessment between two binaries.
|
||||
/// </summary>
|
||||
public sealed record AbiCompatibility
|
||||
{
|
||||
/// <summary>Overall compatibility level.</summary>
|
||||
[JsonPropertyName("level")]
|
||||
public required AbiCompatibilityLevel Level { get; init; }
|
||||
|
||||
/// <summary>Compatibility score (0.0 = incompatible, 1.0 = fully compatible).</summary>
|
||||
[JsonPropertyName("score")]
|
||||
public double Score { get; init; }
|
||||
|
||||
/// <summary>Whether the target is backward compatible with base.</summary>
|
||||
[JsonPropertyName("is_backward_compatible")]
|
||||
public bool IsBackwardCompatible { get; init; }
|
||||
|
||||
/// <summary>Whether the target is forward compatible with base.</summary>
|
||||
[JsonPropertyName("is_forward_compatible")]
|
||||
public bool IsForwardCompatible { get; init; }
|
||||
|
||||
/// <summary>List of breaking changes.</summary>
|
||||
[JsonPropertyName("breaking_changes")]
|
||||
public required IReadOnlyList<AbiBreakingChange> BreakingChanges { get; init; }
|
||||
|
||||
/// <summary>List of compatibility warnings.</summary>
|
||||
[JsonPropertyName("warnings")]
|
||||
public required IReadOnlyList<AbiWarning> Warnings { get; init; }
|
||||
|
||||
/// <summary>Summary statistics.</summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public required AbiSummary Summary { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>ABI compatibility level.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AbiCompatibilityLevel
|
||||
{
|
||||
/// <summary>Fully compatible - no breaking changes.</summary>
|
||||
FullyCompatible,
|
||||
|
||||
/// <summary>Compatible with warnings - minor changes detected.</summary>
|
||||
CompatibleWithWarnings,
|
||||
|
||||
/// <summary>Minor incompatibility - some breaking changes.</summary>
|
||||
MinorIncompatibility,
|
||||
|
||||
/// <summary>Major incompatibility - significant breaking changes.</summary>
|
||||
MajorIncompatibility,
|
||||
|
||||
/// <summary>Not compatible - complete ABI break.</summary>
|
||||
Incompatible
|
||||
}
|
||||
|
||||
/// <summary>A specific ABI breaking change.</summary>
|
||||
public sealed record AbiBreakingChange
|
||||
{
|
||||
/// <summary>Type of breaking change.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required AbiBreakType Type { get; init; }
|
||||
|
||||
/// <summary>Severity of the break.</summary>
|
||||
[JsonPropertyName("severity")]
|
||||
public required ChangeSeverity Severity { get; init; }
|
||||
|
||||
/// <summary>Symbol or entity affected.</summary>
|
||||
[JsonPropertyName("symbol")]
|
||||
public required string Symbol { get; init; }
|
||||
|
||||
/// <summary>Human-readable description.</summary>
|
||||
[JsonPropertyName("description")]
|
||||
public required string Description { get; init; }
|
||||
|
||||
/// <summary>Detailed context.</summary>
|
||||
[JsonPropertyName("details")]
|
||||
public string? Details { get; init; }
|
||||
|
||||
/// <summary>Potential impact.</summary>
|
||||
[JsonPropertyName("impact")]
|
||||
public string? Impact { get; init; }
|
||||
|
||||
/// <summary>Suggested mitigation.</summary>
|
||||
[JsonPropertyName("mitigation")]
|
||||
public string? Mitigation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Type of ABI breaking change.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AbiBreakType
|
||||
{
|
||||
/// <summary>Symbol was removed.</summary>
|
||||
SymbolRemoved,
|
||||
|
||||
/// <summary>Symbol type changed.</summary>
|
||||
SymbolTypeChanged,
|
||||
|
||||
/// <summary>Symbol size changed.</summary>
|
||||
SymbolSizeChanged,
|
||||
|
||||
/// <summary>Symbol visibility reduced.</summary>
|
||||
VisibilityReduced,
|
||||
|
||||
/// <summary>Symbol binding changed.</summary>
|
||||
BindingChanged,
|
||||
|
||||
/// <summary>Version removed.</summary>
|
||||
VersionRemoved,
|
||||
|
||||
/// <summary>Version requirement added.</summary>
|
||||
VersionRequirementAdded,
|
||||
|
||||
/// <summary>Library dependency removed.</summary>
|
||||
LibraryRemoved,
|
||||
|
||||
/// <summary>Library dependency added.</summary>
|
||||
LibraryAdded,
|
||||
|
||||
/// <summary>Function signature changed (inferred).</summary>
|
||||
SignatureChanged,
|
||||
|
||||
/// <summary>Data layout changed (inferred).</summary>
|
||||
DataLayoutChanged,
|
||||
|
||||
/// <summary>TLS model changed.</summary>
|
||||
TlsModelChanged
|
||||
}
|
||||
|
||||
/// <summary>An ABI warning (non-breaking but notable).</summary>
|
||||
public sealed record AbiWarning
|
||||
{
|
||||
/// <summary>Warning type.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required AbiWarningType Type { get; init; }
|
||||
|
||||
/// <summary>Symbol or entity affected.</summary>
|
||||
[JsonPropertyName("symbol")]
|
||||
public string? Symbol { get; init; }
|
||||
|
||||
/// <summary>Warning message.</summary>
|
||||
[JsonPropertyName("message")]
|
||||
public required string Message { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Type of ABI warning.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum AbiWarningType
|
||||
{
|
||||
/// <summary>Symbol was added.</summary>
|
||||
SymbolAdded,
|
||||
|
||||
/// <summary>Symbol visibility increased.</summary>
|
||||
VisibilityIncreased,
|
||||
|
||||
/// <summary>New version definition.</summary>
|
||||
VersionAdded,
|
||||
|
||||
/// <summary>Symbol renamed.</summary>
|
||||
SymbolRenamed,
|
||||
|
||||
/// <summary>Size increased (backward compatible).</summary>
|
||||
SizeIncreased,
|
||||
|
||||
/// <summary>Address changed.</summary>
|
||||
AddressChanged,
|
||||
|
||||
/// <summary>Section changed.</summary>
|
||||
SectionChanged
|
||||
}
|
||||
|
||||
/// <summary>Summary statistics for ABI assessment.</summary>
|
||||
public sealed record AbiSummary
|
||||
{
|
||||
[JsonPropertyName("total_exports_base")]
|
||||
public int TotalExportsBase { get; init; }
|
||||
|
||||
[JsonPropertyName("total_exports_target")]
|
||||
public int TotalExportsTarget { get; init; }
|
||||
|
||||
[JsonPropertyName("exports_added")]
|
||||
public int ExportsAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("exports_removed")]
|
||||
public int ExportsRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("exports_modified")]
|
||||
public int ExportsModified { get; init; }
|
||||
|
||||
[JsonPropertyName("breaking_changes_count")]
|
||||
public int BreakingChangesCount { get; init; }
|
||||
|
||||
[JsonPropertyName("warnings_count")]
|
||||
public int WarningsCount { get; init; }
|
||||
|
||||
[JsonPropertyName("compatibility_percentage")]
|
||||
public double CompatibilityPercentage { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DynamicLinkingDiff.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Task: SYM-004 - Define DynamicLinkingDiff records (GOT/PLT)
|
||||
// Description: Dynamic linking diff model for GOT/PLT changes
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Diff of dynamic linking structures (GOT/PLT) between binaries.
|
||||
/// </summary>
|
||||
public sealed record DynamicLinkingDiff
|
||||
{
|
||||
/// <summary>GOT (Global Offset Table) changes.</summary>
|
||||
[JsonPropertyName("got")]
|
||||
public required GotDiff Got { get; init; }
|
||||
|
||||
/// <summary>PLT (Procedure Linkage Table) changes.</summary>
|
||||
[JsonPropertyName("plt")]
|
||||
public required PltDiff Plt { get; init; }
|
||||
|
||||
/// <summary>RPATH/RUNPATH changes.</summary>
|
||||
[JsonPropertyName("rpath")]
|
||||
public required RpathDiff Rpath { get; init; }
|
||||
|
||||
/// <summary>NEEDED library changes.</summary>
|
||||
[JsonPropertyName("needed")]
|
||||
public required NeededDiff Needed { get; init; }
|
||||
|
||||
/// <summary>Relocation changes.</summary>
|
||||
[JsonPropertyName("relocations")]
|
||||
public RelocationDiff? Relocations { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>GOT (Global Offset Table) diff.</summary>
|
||||
public sealed record GotDiff
|
||||
{
|
||||
[JsonPropertyName("entries_added")]
|
||||
public required IReadOnlyList<GotEntry> EntriesAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("entries_removed")]
|
||||
public required IReadOnlyList<GotEntry> EntriesRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("entries_modified")]
|
||||
public required IReadOnlyList<GotEntryModification> EntriesModified { get; init; }
|
||||
|
||||
[JsonPropertyName("base_count")]
|
||||
public int BaseCount { get; init; }
|
||||
|
||||
[JsonPropertyName("target_count")]
|
||||
public int TargetCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A GOT entry.</summary>
|
||||
public sealed record GotEntry
|
||||
{
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; init; }
|
||||
|
||||
[JsonPropertyName("symbol_name")]
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
[JsonPropertyName("address")]
|
||||
public ulong Address { get; init; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public required GotEntryType Type { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A GOT entry modification.</summary>
|
||||
public sealed record GotEntryModification
|
||||
{
|
||||
[JsonPropertyName("symbol_name")]
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
[JsonPropertyName("base_address")]
|
||||
public ulong BaseAddress { get; init; }
|
||||
|
||||
[JsonPropertyName("target_address")]
|
||||
public ulong TargetAddress { get; init; }
|
||||
|
||||
[JsonPropertyName("base_type")]
|
||||
public required GotEntryType BaseType { get; init; }
|
||||
|
||||
[JsonPropertyName("target_type")]
|
||||
public required GotEntryType TargetType { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>GOT entry type.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum GotEntryType
|
||||
{
|
||||
GlobDat,
|
||||
JumpSlot,
|
||||
Relative,
|
||||
Copy,
|
||||
TlsDtpMod,
|
||||
TlsDtpOff,
|
||||
TlsTpOff,
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>PLT (Procedure Linkage Table) diff.</summary>
|
||||
public sealed record PltDiff
|
||||
{
|
||||
[JsonPropertyName("entries_added")]
|
||||
public required IReadOnlyList<PltEntry> EntriesAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("entries_removed")]
|
||||
public required IReadOnlyList<PltEntry> EntriesRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("entries_reordered")]
|
||||
public required IReadOnlyList<PltReorder> EntriesReordered { get; init; }
|
||||
|
||||
[JsonPropertyName("base_count")]
|
||||
public int BaseCount { get; init; }
|
||||
|
||||
[JsonPropertyName("target_count")]
|
||||
public int TargetCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A PLT entry.</summary>
|
||||
public sealed record PltEntry
|
||||
{
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; init; }
|
||||
|
||||
[JsonPropertyName("symbol_name")]
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
[JsonPropertyName("address")]
|
||||
public ulong Address { get; init; }
|
||||
|
||||
[JsonPropertyName("got_offset")]
|
||||
public ulong GotOffset { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A PLT entry reordering.</summary>
|
||||
public sealed record PltReorder
|
||||
{
|
||||
[JsonPropertyName("symbol_name")]
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
[JsonPropertyName("base_index")]
|
||||
public int BaseIndex { get; init; }
|
||||
|
||||
[JsonPropertyName("target_index")]
|
||||
public int TargetIndex { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>RPATH/RUNPATH diff.</summary>
|
||||
public sealed record RpathDiff
|
||||
{
|
||||
[JsonPropertyName("rpath_base")]
|
||||
public IReadOnlyList<string>? RpathBase { get; init; }
|
||||
|
||||
[JsonPropertyName("rpath_target")]
|
||||
public IReadOnlyList<string>? RpathTarget { get; init; }
|
||||
|
||||
[JsonPropertyName("runpath_base")]
|
||||
public IReadOnlyList<string>? RunpathBase { get; init; }
|
||||
|
||||
[JsonPropertyName("runpath_target")]
|
||||
public IReadOnlyList<string>? RunpathTarget { get; init; }
|
||||
|
||||
[JsonPropertyName("paths_added")]
|
||||
public required IReadOnlyList<string> PathsAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("paths_removed")]
|
||||
public required IReadOnlyList<string> PathsRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("has_changes")]
|
||||
public bool HasChanges { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>NEEDED library diff.</summary>
|
||||
public sealed record NeededDiff
|
||||
{
|
||||
[JsonPropertyName("libraries_added")]
|
||||
public required IReadOnlyList<string> LibrariesAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("libraries_removed")]
|
||||
public required IReadOnlyList<string> LibrariesRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("base_libraries")]
|
||||
public required IReadOnlyList<string> BaseLibraries { get; init; }
|
||||
|
||||
[JsonPropertyName("target_libraries")]
|
||||
public required IReadOnlyList<string> TargetLibraries { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Relocation diff (optional, detailed).</summary>
|
||||
public sealed record RelocationDiff
|
||||
{
|
||||
[JsonPropertyName("relocations_added")]
|
||||
public int RelocationsAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("relocations_removed")]
|
||||
public int RelocationsRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("relocations_modified")]
|
||||
public int RelocationsModified { get; init; }
|
||||
|
||||
[JsonPropertyName("base_count")]
|
||||
public int BaseCount { get; init; }
|
||||
|
||||
[JsonPropertyName("target_count")]
|
||||
public int TargetCount { get; init; }
|
||||
|
||||
[JsonPropertyName("significant_changes")]
|
||||
public required IReadOnlyList<RelocationChange> SignificantChanges { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A significant relocation change.</summary>
|
||||
public sealed record RelocationChange
|
||||
{
|
||||
[JsonPropertyName("symbol_name")]
|
||||
public string? SymbolName { get; init; }
|
||||
|
||||
[JsonPropertyName("change_type")]
|
||||
public required RelocationChangeType ChangeType { get; init; }
|
||||
|
||||
[JsonPropertyName("base_type")]
|
||||
public string? BaseType { get; init; }
|
||||
|
||||
[JsonPropertyName("target_type")]
|
||||
public string? TargetType { get; init; }
|
||||
|
||||
[JsonPropertyName("address")]
|
||||
public ulong Address { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Relocation change type.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum RelocationChangeType
|
||||
{
|
||||
Added,
|
||||
Removed,
|
||||
TypeChanged,
|
||||
SymbolChanged
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ISymbolTableDiffAnalyzer.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Task: SYM-006 - Define ISymbolTableDiffAnalyzer interface
|
||||
// Description: Interface for symbol table diff analysis
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes symbol table differences between two binaries.
|
||||
/// </summary>
|
||||
public interface ISymbolTableDiffAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a complete symbol table diff between base and target binaries.
|
||||
/// </summary>
|
||||
/// <param name="basePath">Path to the base binary.</param>
|
||||
/// <param name="targetPath">Path to the target binary.</param>
|
||||
/// <param name="options">Analysis options.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Symbol table diff.</returns>
|
||||
Task<SymbolTableDiff> ComputeDiffAsync(
|
||||
string basePath,
|
||||
string targetPath,
|
||||
SymbolDiffOptions? options = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts symbol table from a single binary.
|
||||
/// </summary>
|
||||
/// <param name="binaryPath">Path to the binary.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Extracted symbol table.</returns>
|
||||
Task<SymbolTable> ExtractSymbolTableAsync(
|
||||
string binaryPath,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Computes ABI compatibility assessment.
|
||||
/// </summary>
|
||||
/// <param name="diff">The symbol table diff.</param>
|
||||
/// <returns>ABI compatibility assessment.</returns>
|
||||
AbiCompatibility AssessAbiCompatibility(SymbolTableDiff diff);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for symbol diff analysis.
|
||||
/// </summary>
|
||||
public sealed record SymbolDiffOptions
|
||||
{
|
||||
/// <summary>Whether to include dynamic linking analysis (GOT/PLT).</summary>
|
||||
public bool IncludeDynamicLinking { get; init; } = true;
|
||||
|
||||
/// <summary>Whether to detect symbol renames via fingerprinting.</summary>
|
||||
public bool DetectRenames { get; init; } = true;
|
||||
|
||||
/// <summary>Minimum similarity threshold for rename detection (0.0-1.0).</summary>
|
||||
public double RenameSimilarityThreshold { get; init; } = 0.7;
|
||||
|
||||
/// <summary>Whether to demangle C++/Rust names.</summary>
|
||||
public bool DemangleNames { get; init; } = true;
|
||||
|
||||
/// <summary>Whether to compute function fingerprints for modified symbols.</summary>
|
||||
public bool ComputeFingerprints { get; init; } = true;
|
||||
|
||||
/// <summary>Maximum symbols to process (for large binaries).</summary>
|
||||
public int? MaxSymbols { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracted symbol table from a binary.
|
||||
/// </summary>
|
||||
public sealed record SymbolTable
|
||||
{
|
||||
/// <summary>Binary reference.</summary>
|
||||
public required BinaryRef Binary { get; init; }
|
||||
|
||||
/// <summary>Exported symbols (.dynsym exports for ELF, exports for PE).</summary>
|
||||
public required IReadOnlyList<ExtractedSymbol> Exports { get; init; }
|
||||
|
||||
/// <summary>Imported symbols.</summary>
|
||||
public required IReadOnlyList<ExtractedSymbol> Imports { get; init; }
|
||||
|
||||
/// <summary>Version definitions (.gnu.version_d for ELF).</summary>
|
||||
public required IReadOnlyList<VersionDefinition> VersionDefinitions { get; init; }
|
||||
|
||||
/// <summary>Version requirements (.gnu.version_r for ELF).</summary>
|
||||
public required IReadOnlyList<VersionRequirement> VersionRequirements { get; init; }
|
||||
|
||||
/// <summary>GOT entries.</summary>
|
||||
public IReadOnlyList<GotEntry>? GotEntries { get; init; }
|
||||
|
||||
/// <summary>PLT entries.</summary>
|
||||
public IReadOnlyList<PltEntry>? PltEntries { get; init; }
|
||||
|
||||
/// <summary>NEEDED libraries.</summary>
|
||||
public required IReadOnlyList<string> NeededLibraries { get; init; }
|
||||
|
||||
/// <summary>RPATH entries.</summary>
|
||||
public IReadOnlyList<string>? Rpath { get; init; }
|
||||
|
||||
/// <summary>RUNPATH entries.</summary>
|
||||
public IReadOnlyList<string>? Runpath { get; init; }
|
||||
|
||||
/// <summary>When extracted.</summary>
|
||||
public required DateTimeOffset ExtractedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An extracted symbol with all metadata.
|
||||
/// </summary>
|
||||
public sealed record ExtractedSymbol
|
||||
{
|
||||
/// <summary>Mangled name.</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>Demangled name (if available).</summary>
|
||||
public string? DemangledName { get; init; }
|
||||
|
||||
/// <summary>Symbol type.</summary>
|
||||
public required SymbolType Type { get; init; }
|
||||
|
||||
/// <summary>Symbol binding.</summary>
|
||||
public required SymbolBinding Binding { get; init; }
|
||||
|
||||
/// <summary>Symbol visibility.</summary>
|
||||
public required SymbolVisibility Visibility { get; init; }
|
||||
|
||||
/// <summary>Section name.</summary>
|
||||
public string? Section { get; init; }
|
||||
|
||||
/// <summary>Section index.</summary>
|
||||
public int SectionIndex { get; init; }
|
||||
|
||||
/// <summary>Virtual address.</summary>
|
||||
public ulong Address { get; init; }
|
||||
|
||||
/// <summary>Symbol size.</summary>
|
||||
public ulong Size { get; init; }
|
||||
|
||||
/// <summary>Version string (from .gnu.version).</summary>
|
||||
public string? Version { get; init; }
|
||||
|
||||
/// <summary>Version index.</summary>
|
||||
public int VersionIndex { get; init; }
|
||||
|
||||
/// <summary>Whether this is a hidden version.</summary>
|
||||
public bool IsVersionHidden { get; init; }
|
||||
|
||||
/// <summary>Function fingerprint (for functions).</summary>
|
||||
public string? Fingerprint { get; init; }
|
||||
|
||||
/// <summary>Whether this is a TLS symbol.</summary>
|
||||
public bool IsTls { get; init; }
|
||||
|
||||
/// <summary>Whether this is a weak symbol.</summary>
|
||||
public bool IsWeak => Binding == SymbolBinding.Weak;
|
||||
|
||||
/// <summary>Whether this is a function.</summary>
|
||||
public bool IsFunction => Type == SymbolType.Function;
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NameDemangler.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Tasks: SYM-016, SYM-017 - C++ and Rust name demangling support
|
||||
// Description: Name demangler implementation for C++ and Rust symbols
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Demangles C++ and Rust symbol names.
|
||||
/// </summary>
|
||||
public sealed partial class NameDemangler : INameDemangler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string? Demangle(string mangledName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mangledName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var scheme = DetectScheme(mangledName);
|
||||
|
||||
return scheme switch
|
||||
{
|
||||
ManglingScheme.ItaniumCxx => DemangleItaniumCxx(mangledName),
|
||||
ManglingScheme.MicrosoftCxx => DemangleMicrosoftCxx(mangledName),
|
||||
ManglingScheme.Rust => DemangleRust(mangledName),
|
||||
ManglingScheme.Swift => DemangleSwift(mangledName),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ManglingScheme DetectScheme(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return ManglingScheme.None;
|
||||
}
|
||||
|
||||
// Itanium C++ ABI: starts with _Z
|
||||
if (name.StartsWith("_Z", StringComparison.Ordinal))
|
||||
{
|
||||
return ManglingScheme.ItaniumCxx;
|
||||
}
|
||||
|
||||
// Microsoft C++ ABI: starts with ?
|
||||
if (name.StartsWith('?'))
|
||||
{
|
||||
return ManglingScheme.MicrosoftCxx;
|
||||
}
|
||||
|
||||
// Rust legacy: starts with _ZN...E or contains $
|
||||
if (name.StartsWith("_ZN", StringComparison.Ordinal) && name.Contains("17h"))
|
||||
{
|
||||
return ManglingScheme.Rust;
|
||||
}
|
||||
|
||||
// Rust v0: starts with _R
|
||||
if (name.StartsWith("_R", StringComparison.Ordinal))
|
||||
{
|
||||
return ManglingScheme.Rust;
|
||||
}
|
||||
|
||||
// Swift: starts with $s or _$s
|
||||
if (name.StartsWith("$s", StringComparison.Ordinal) ||
|
||||
name.StartsWith("_$s", StringComparison.Ordinal))
|
||||
{
|
||||
return ManglingScheme.Swift;
|
||||
}
|
||||
|
||||
// Plain C or unknown
|
||||
return ManglingScheme.None;
|
||||
}
|
||||
|
||||
// SYM-016: C++ name demangling (Itanium ABI - GCC/Clang)
|
||||
private static string? DemangleItaniumCxx(string mangledName)
|
||||
{
|
||||
// This is a simplified demangler for common patterns
|
||||
// Production code should use a full demangler library (e.g., cxxfilt, llvm-cxxfilt)
|
||||
|
||||
if (!mangledName.StartsWith("_Z", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = new System.Text.StringBuilder();
|
||||
var pos = 2; // Skip _Z
|
||||
|
||||
// Parse nested name if present
|
||||
if (pos < mangledName.Length && mangledName[pos] == 'N')
|
||||
{
|
||||
pos++; // Skip N
|
||||
|
||||
// Parse CV qualifiers
|
||||
while (pos < mangledName.Length && (mangledName[pos] == 'K' || mangledName[pos] == 'V' || mangledName[pos] == 'r'))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
|
||||
// Parse name parts until E
|
||||
while (pos < mangledName.Length && mangledName[pos] != 'E')
|
||||
{
|
||||
var (name, newPos) = ParseNamePart(mangledName, pos);
|
||||
if (name is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
parts.Add(name);
|
||||
pos = newPos;
|
||||
}
|
||||
|
||||
result.Append(string.Join("::", parts));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple name
|
||||
var (name, _) = ParseNamePart(mangledName, pos);
|
||||
if (name is not null)
|
||||
{
|
||||
result.Append(name);
|
||||
}
|
||||
}
|
||||
|
||||
var demangled = result.ToString();
|
||||
return string.IsNullOrEmpty(demangled) ? null : demangled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static (string? Name, int NewPos) ParseNamePart(string mangled, int pos)
|
||||
{
|
||||
if (pos >= mangled.Length)
|
||||
{
|
||||
return (null, pos);
|
||||
}
|
||||
|
||||
// Check for special prefixes
|
||||
if (mangled[pos] == 'S')
|
||||
{
|
||||
// Substitution - simplified handling
|
||||
pos++;
|
||||
if (pos < mangled.Length && mangled[pos] == 't')
|
||||
{
|
||||
return ("std", pos + 1);
|
||||
}
|
||||
if (pos < mangled.Length && mangled[pos] == 's')
|
||||
{
|
||||
return ("std::string", pos + 1);
|
||||
}
|
||||
// Skip substitution index
|
||||
while (pos < mangled.Length && char.IsLetterOrDigit(mangled[pos]) && mangled[pos] != '_')
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
if (pos < mangled.Length && mangled[pos] == '_')
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
return (null, pos);
|
||||
}
|
||||
|
||||
// Parse length-prefixed name
|
||||
var lengthStart = pos;
|
||||
while (pos < mangled.Length && char.IsDigit(mangled[pos]))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (lengthStart == pos)
|
||||
{
|
||||
return (null, pos);
|
||||
}
|
||||
|
||||
if (!int.TryParse(mangled.AsSpan(lengthStart, pos - lengthStart), out var length))
|
||||
{
|
||||
return (null, pos);
|
||||
}
|
||||
|
||||
if (pos + length > mangled.Length)
|
||||
{
|
||||
return (null, pos);
|
||||
}
|
||||
|
||||
var name = mangled.Substring(pos, length);
|
||||
return (name, pos + length);
|
||||
}
|
||||
|
||||
// Microsoft C++ demangling (simplified)
|
||||
private static string? DemangleMicrosoftCxx(string mangledName)
|
||||
{
|
||||
// This is a very simplified demangler
|
||||
// Production code should use undname.exe or a full implementation
|
||||
|
||||
if (!mangledName.StartsWith('?'))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Extract the basic name between ? and @
|
||||
var firstAt = mangledName.IndexOf('@', 1);
|
||||
if (firstAt < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = mangledName[1..firstAt];
|
||||
|
||||
// Find namespace parts (between @ symbols)
|
||||
var parts = new List<string> { name };
|
||||
var pos = firstAt + 1;
|
||||
|
||||
while (pos < mangledName.Length && mangledName[pos] != '@')
|
||||
{
|
||||
var nextAt = mangledName.IndexOf('@', pos);
|
||||
if (nextAt < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var part = mangledName[pos..nextAt];
|
||||
if (!string.IsNullOrEmpty(part) && char.IsLetter(part[0]))
|
||||
{
|
||||
parts.Insert(0, part);
|
||||
}
|
||||
pos = nextAt + 1;
|
||||
}
|
||||
|
||||
return string.Join("::", parts);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// SYM-017: Rust name demangling
|
||||
private static string? DemangleRust(string mangledName)
|
||||
{
|
||||
// Rust legacy mangling: _ZN<len>name...<len>name17h<hash>E
|
||||
// Rust v0 mangling: _R<...>
|
||||
|
||||
try
|
||||
{
|
||||
if (mangledName.StartsWith("_R", StringComparison.Ordinal))
|
||||
{
|
||||
return DemangleRustV0(mangledName);
|
||||
}
|
||||
|
||||
if (mangledName.StartsWith("_ZN", StringComparison.Ordinal))
|
||||
{
|
||||
return DemangleRustLegacy(mangledName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? DemangleRustLegacy(string mangledName)
|
||||
{
|
||||
// Format: _ZN<parts>17h<16-hex-digits>E
|
||||
|
||||
if (!mangledName.StartsWith("_ZN", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pos = 3; // Skip _ZN
|
||||
var parts = new List<string>();
|
||||
|
||||
while (pos < mangledName.Length && mangledName[pos] != 'E')
|
||||
{
|
||||
// Parse length
|
||||
var lengthStart = pos;
|
||||
while (pos < mangledName.Length && char.IsDigit(mangledName[pos]))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (lengthStart == pos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!int.TryParse(mangledName.AsSpan(lengthStart, pos - lengthStart), out var length))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (pos + length > mangledName.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var part = mangledName.Substring(pos, length);
|
||||
pos += length;
|
||||
|
||||
// Skip hash part (17h + 16 hex digits)
|
||||
if (part.StartsWith('h') && part.Length == 17 && IsHexString().IsMatch(part[1..]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode Rust escapes: $LT$ -> <, $GT$ -> >, $BP$ -> *, etc.
|
||||
part = DecodeRustEscapes(part);
|
||||
parts.Add(part);
|
||||
}
|
||||
|
||||
return parts.Count > 0 ? string.Join("::", parts) : null;
|
||||
}
|
||||
|
||||
private static string? DemangleRustV0(string mangledName)
|
||||
{
|
||||
// Rust v0 mangling is complex; provide basic support
|
||||
// Full implementation would require the v0 demangling spec
|
||||
|
||||
if (!mangledName.StartsWith("_R", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Very simplified: just extract what we can
|
||||
// In production, use rust-demangle crate via P/Invoke or subprocess
|
||||
return $"<rust-v0>{mangledName[2..]}";
|
||||
}
|
||||
|
||||
private static string DecodeRustEscapes(string input)
|
||||
{
|
||||
return input
|
||||
.Replace("$LT$", "<")
|
||||
.Replace("$GT$", ">")
|
||||
.Replace("$BP$", "*")
|
||||
.Replace("$RF$", "&")
|
||||
.Replace("$LP$", "(")
|
||||
.Replace("$RP$", ")")
|
||||
.Replace("$C$", ",")
|
||||
.Replace("$SP$", "@")
|
||||
.Replace("..", "::");
|
||||
}
|
||||
|
||||
// Swift demangling (placeholder)
|
||||
private static string? DemangleSwift(string mangledName)
|
||||
{
|
||||
// Swift demangling is very complex
|
||||
// In production, use swift-demangle via subprocess
|
||||
|
||||
if (mangledName.StartsWith("$s", StringComparison.Ordinal))
|
||||
{
|
||||
return $"<swift>{mangledName[2..]}";
|
||||
}
|
||||
|
||||
if (mangledName.StartsWith("_$s", StringComparison.Ordinal))
|
||||
{
|
||||
return $"<swift>{mangledName[3..]}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[0-9a-f]+$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex IsHexString();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SymbolDiffServiceExtensions.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Task: SYM-019 - Add service registration extensions
|
||||
// Description: DI registration extensions for symbol diff services
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Service collection extensions for symbol diff analyzer.
|
||||
/// </summary>
|
||||
public static class SymbolDiffServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds symbol table diff analyzer services.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolTableDiffAnalyzer(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<INameDemangler, NameDemangler>();
|
||||
services.AddSingleton<ISymbolTableDiffAnalyzer, SymbolTableDiffAnalyzer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds symbol table diff analyzer with custom symbol extractor.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolTableDiffAnalyzer<TExtractor>(this IServiceCollection services)
|
||||
where TExtractor : class, ISymbolExtractor
|
||||
{
|
||||
services.AddSingleton<ISymbolExtractor, TExtractor>();
|
||||
services.AddSingleton<INameDemangler, NameDemangler>();
|
||||
services.AddSingleton<ISymbolTableDiffAnalyzer, SymbolTableDiffAnalyzer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SymbolTableDiff.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Tasks: SYM-001, SYM-002, SYM-003, SYM-004, SYM-005
|
||||
// Description: Symbol table diff model for comparing exports/imports between binaries
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Complete symbol table diff between two binaries.
|
||||
/// </summary>
|
||||
public sealed record SymbolTableDiff
|
||||
{
|
||||
/// <summary>Content-addressed diff ID (sha256 of canonical JSON).</summary>
|
||||
[JsonPropertyName("diff_id")]
|
||||
public required string DiffId { get; init; }
|
||||
|
||||
/// <summary>Base binary identity.</summary>
|
||||
[JsonPropertyName("base")]
|
||||
public required BinaryRef Base { get; init; }
|
||||
|
||||
/// <summary>Target binary identity.</summary>
|
||||
[JsonPropertyName("target")]
|
||||
public required BinaryRef Target { get; init; }
|
||||
|
||||
/// <summary>Exported symbol changes.</summary>
|
||||
[JsonPropertyName("exports")]
|
||||
public required SymbolChangeSummary Exports { get; init; }
|
||||
|
||||
/// <summary>Imported symbol changes.</summary>
|
||||
[JsonPropertyName("imports")]
|
||||
public required SymbolChangeSummary Imports { get; init; }
|
||||
|
||||
/// <summary>Version map changes.</summary>
|
||||
[JsonPropertyName("versions")]
|
||||
public required VersionMapDiff Versions { get; init; }
|
||||
|
||||
/// <summary>GOT/PLT changes (dynamic linking).</summary>
|
||||
[JsonPropertyName("dynamic")]
|
||||
public DynamicLinkingDiff? Dynamic { get; init; }
|
||||
|
||||
/// <summary>Overall ABI compatibility assessment.</summary>
|
||||
[JsonPropertyName("abi_compatibility")]
|
||||
public required AbiCompatibility AbiCompatibility { get; init; }
|
||||
|
||||
/// <summary>When this diff was computed (UTC).</summary>
|
||||
[JsonPropertyName("computed_at")]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
/// <summary>Schema version for forward compatibility.</summary>
|
||||
[JsonPropertyName("schema_version")]
|
||||
public string SchemaVersion { get; init; } = "1.0";
|
||||
}
|
||||
|
||||
/// <summary>Reference to a binary.</summary>
|
||||
public sealed record BinaryRef
|
||||
{
|
||||
[JsonPropertyName("path")]
|
||||
public required string Path { get; init; }
|
||||
|
||||
[JsonPropertyName("sha256")]
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
[JsonPropertyName("build_id")]
|
||||
public string? BuildId { get; init; }
|
||||
|
||||
[JsonPropertyName("architecture")]
|
||||
public required string Architecture { get; init; }
|
||||
|
||||
[JsonPropertyName("format")]
|
||||
public required BinaryFormat Format { get; init; }
|
||||
|
||||
[JsonPropertyName("file_size")]
|
||||
public long FileSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Binary format.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum BinaryFormat
|
||||
{
|
||||
Elf,
|
||||
Pe,
|
||||
MachO,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>Summary of symbol changes.</summary>
|
||||
public sealed record SymbolChangeSummary
|
||||
{
|
||||
[JsonPropertyName("added")]
|
||||
public required IReadOnlyList<SymbolChange> Added { get; init; }
|
||||
|
||||
[JsonPropertyName("removed")]
|
||||
public required IReadOnlyList<SymbolChange> Removed { get; init; }
|
||||
|
||||
[JsonPropertyName("modified")]
|
||||
public required IReadOnlyList<SymbolModification> Modified { get; init; }
|
||||
|
||||
[JsonPropertyName("renamed")]
|
||||
public required IReadOnlyList<SymbolRename> Renamed { get; init; }
|
||||
|
||||
/// <summary>Count summaries.</summary>
|
||||
[JsonPropertyName("counts")]
|
||||
public required SymbolChangeCounts Counts { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Count summary for symbol changes.</summary>
|
||||
public sealed record SymbolChangeCounts
|
||||
{
|
||||
[JsonPropertyName("added")]
|
||||
public int Added { get; init; }
|
||||
|
||||
[JsonPropertyName("removed")]
|
||||
public int Removed { get; init; }
|
||||
|
||||
[JsonPropertyName("modified")]
|
||||
public int Modified { get; init; }
|
||||
|
||||
[JsonPropertyName("renamed")]
|
||||
public int Renamed { get; init; }
|
||||
|
||||
[JsonPropertyName("unchanged")]
|
||||
public int Unchanged { get; init; }
|
||||
|
||||
[JsonPropertyName("total_base")]
|
||||
public int TotalBase { get; init; }
|
||||
|
||||
[JsonPropertyName("total_target")]
|
||||
public int TotalTarget { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A symbol that was added or removed.</summary>
|
||||
public sealed record SymbolChange
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("demangled_name")]
|
||||
public string? DemangledName { get; init; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public required SymbolType Type { get; init; }
|
||||
|
||||
[JsonPropertyName("binding")]
|
||||
public required SymbolBinding Binding { get; init; }
|
||||
|
||||
[JsonPropertyName("visibility")]
|
||||
public required SymbolVisibility Visibility { get; init; }
|
||||
|
||||
[JsonPropertyName("section")]
|
||||
public string? Section { get; init; }
|
||||
|
||||
[JsonPropertyName("address")]
|
||||
public ulong Address { get; init; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public ulong Size { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; init; }
|
||||
|
||||
[JsonPropertyName("fingerprint")]
|
||||
public string? Fingerprint { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A symbol that was modified (same name, different attributes).</summary>
|
||||
public sealed record SymbolModification
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("demangled_name")]
|
||||
public string? DemangledName { get; init; }
|
||||
|
||||
[JsonPropertyName("base")]
|
||||
public required SymbolAttributes Base { get; init; }
|
||||
|
||||
[JsonPropertyName("target")]
|
||||
public required SymbolAttributes Target { get; init; }
|
||||
|
||||
[JsonPropertyName("changes")]
|
||||
public required IReadOnlyList<AttributeChange> Changes { get; init; }
|
||||
|
||||
[JsonPropertyName("is_abi_breaking")]
|
||||
public bool IsAbiBreaking { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Symbol attributes for comparison.</summary>
|
||||
public sealed record SymbolAttributes
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public required SymbolType Type { get; init; }
|
||||
|
||||
[JsonPropertyName("binding")]
|
||||
public required SymbolBinding Binding { get; init; }
|
||||
|
||||
[JsonPropertyName("visibility")]
|
||||
public required SymbolVisibility Visibility { get; init; }
|
||||
|
||||
[JsonPropertyName("section")]
|
||||
public string? Section { get; init; }
|
||||
|
||||
[JsonPropertyName("address")]
|
||||
public ulong Address { get; init; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public ulong Size { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; init; }
|
||||
|
||||
[JsonPropertyName("fingerprint")]
|
||||
public string? Fingerprint { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A specific attribute change.</summary>
|
||||
public sealed record AttributeChange
|
||||
{
|
||||
[JsonPropertyName("attribute")]
|
||||
public required string Attribute { get; init; }
|
||||
|
||||
[JsonPropertyName("base_value")]
|
||||
public string? BaseValue { get; init; }
|
||||
|
||||
[JsonPropertyName("target_value")]
|
||||
public string? TargetValue { get; init; }
|
||||
|
||||
[JsonPropertyName("severity")]
|
||||
public required ChangeSeverity Severity { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A symbol that was renamed (detected via fingerprint matching).</summary>
|
||||
public sealed record SymbolRename
|
||||
{
|
||||
[JsonPropertyName("base_name")]
|
||||
public required string BaseName { get; init; }
|
||||
|
||||
[JsonPropertyName("target_name")]
|
||||
public required string TargetName { get; init; }
|
||||
|
||||
[JsonPropertyName("base_demangled")]
|
||||
public string? BaseDemangled { get; init; }
|
||||
|
||||
[JsonPropertyName("target_demangled")]
|
||||
public string? TargetDemangled { get; init; }
|
||||
|
||||
[JsonPropertyName("fingerprint")]
|
||||
public required string Fingerprint { get; init; }
|
||||
|
||||
[JsonPropertyName("similarity")]
|
||||
public double Similarity { get; init; }
|
||||
|
||||
[JsonPropertyName("confidence")]
|
||||
public required RenameConfidence Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Symbol type classification.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SymbolType
|
||||
{
|
||||
NoType,
|
||||
Object,
|
||||
Function,
|
||||
Section,
|
||||
File,
|
||||
Common,
|
||||
Tls,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>Symbol binding.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SymbolBinding
|
||||
{
|
||||
Local,
|
||||
Global,
|
||||
Weak,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>Symbol visibility.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum SymbolVisibility
|
||||
{
|
||||
Default,
|
||||
Internal,
|
||||
Hidden,
|
||||
Protected,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>Severity of a change.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum ChangeSeverity
|
||||
{
|
||||
Info,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical
|
||||
}
|
||||
|
||||
/// <summary>Confidence level for rename detection.</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum RenameConfidence
|
||||
{
|
||||
VeryHigh,
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
VeryLow
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SymbolTableDiffAnalyzer.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Tasks: SYM-007 to SYM-015 - Implement symbol table diff analyzer
|
||||
// Description: Symbol table diff analyzer implementation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes symbol table differences between two binaries.
|
||||
/// </summary>
|
||||
public sealed class SymbolTableDiffAnalyzer : ISymbolTableDiffAnalyzer
|
||||
{
|
||||
private readonly ISymbolExtractor _symbolExtractor;
|
||||
private readonly INameDemangler _nameDemangler;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<SymbolTableDiffAnalyzer> _logger;
|
||||
|
||||
public SymbolTableDiffAnalyzer(
|
||||
ISymbolExtractor symbolExtractor,
|
||||
INameDemangler nameDemangler,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<SymbolTableDiffAnalyzer> logger)
|
||||
{
|
||||
_symbolExtractor = symbolExtractor;
|
||||
_nameDemangler = nameDemangler;
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SymbolTableDiff> ComputeDiffAsync(
|
||||
string basePath,
|
||||
string targetPath,
|
||||
SymbolDiffOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
options ??= new SymbolDiffOptions();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
_logger.LogDebug("Computing symbol diff between {Base} and {Target}", basePath, targetPath);
|
||||
|
||||
// Extract symbol tables
|
||||
var baseTable = await ExtractSymbolTableAsync(basePath, ct);
|
||||
var targetTable = await ExtractSymbolTableAsync(targetPath, ct);
|
||||
|
||||
// Compute symbol changes
|
||||
var exports = ComputeSymbolChanges(
|
||||
baseTable.Exports,
|
||||
targetTable.Exports,
|
||||
options);
|
||||
|
||||
var imports = ComputeSymbolChanges(
|
||||
baseTable.Imports,
|
||||
targetTable.Imports,
|
||||
options);
|
||||
|
||||
// Compute version diff
|
||||
var versions = ComputeVersionDiff(baseTable, targetTable);
|
||||
|
||||
// Compute dynamic linking diff
|
||||
DynamicLinkingDiff? dynamic = null;
|
||||
if (options.IncludeDynamicLinking)
|
||||
{
|
||||
dynamic = ComputeDynamicLinkingDiff(baseTable, targetTable);
|
||||
}
|
||||
|
||||
// Create diff without ID first
|
||||
var diffWithoutId = new SymbolTableDiff
|
||||
{
|
||||
DiffId = string.Empty, // Placeholder
|
||||
Base = baseTable.Binary,
|
||||
Target = targetTable.Binary,
|
||||
Exports = exports,
|
||||
Imports = imports,
|
||||
Versions = versions,
|
||||
Dynamic = dynamic,
|
||||
AbiCompatibility = new AbiCompatibility
|
||||
{
|
||||
Level = AbiCompatibilityLevel.FullyCompatible,
|
||||
Score = 1.0,
|
||||
IsBackwardCompatible = true,
|
||||
IsForwardCompatible = true,
|
||||
BreakingChanges = [],
|
||||
Warnings = [],
|
||||
Summary = new AbiSummary()
|
||||
},
|
||||
ComputedAt = now
|
||||
};
|
||||
|
||||
// Assess ABI compatibility
|
||||
var abiCompatibility = AssessAbiCompatibility(diffWithoutId);
|
||||
|
||||
// Compute content-addressed ID
|
||||
var diffId = ComputeDiffId(baseTable.Binary, targetTable.Binary, exports, imports);
|
||||
|
||||
var diff = diffWithoutId with
|
||||
{
|
||||
DiffId = diffId,
|
||||
AbiCompatibility = abiCompatibility
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Symbol diff complete: {ExportsAdded} exports added, {ExportsRemoved} removed, {Level}",
|
||||
exports.Counts.Added,
|
||||
exports.Counts.Removed,
|
||||
abiCompatibility.Level);
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SymbolTable> ExtractSymbolTableAsync(
|
||||
string binaryPath,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return await _symbolExtractor.ExtractAsync(binaryPath, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AbiCompatibility AssessAbiCompatibility(SymbolTableDiff diff)
|
||||
{
|
||||
var breakingChanges = new List<AbiBreakingChange>();
|
||||
var warnings = new List<AbiWarning>();
|
||||
|
||||
// Removed exports are breaking
|
||||
foreach (var removed in diff.Exports.Removed)
|
||||
{
|
||||
breakingChanges.Add(new AbiBreakingChange
|
||||
{
|
||||
Type = AbiBreakType.SymbolRemoved,
|
||||
Severity = removed.Binding == SymbolBinding.Weak ? ChangeSeverity.Low : ChangeSeverity.High,
|
||||
Symbol = removed.Name,
|
||||
Description = $"Exported symbol '{removed.DemangledName ?? removed.Name}' was removed",
|
||||
Impact = "Code linking against this symbol will fail at runtime",
|
||||
Mitigation = "Provide symbol alias or versioned symbol for backward compatibility"
|
||||
});
|
||||
}
|
||||
|
||||
// Modified exports with size changes
|
||||
foreach (var modified in diff.Exports.Modified)
|
||||
{
|
||||
if (modified.IsAbiBreaking)
|
||||
{
|
||||
foreach (var change in modified.Changes.Where(c => c.Severity >= ChangeSeverity.High))
|
||||
{
|
||||
breakingChanges.Add(new AbiBreakingChange
|
||||
{
|
||||
Type = DetermineBreakType(change),
|
||||
Severity = change.Severity,
|
||||
Symbol = modified.Name,
|
||||
Description = $"Symbol '{modified.DemangledName ?? modified.Name}' {change.Attribute} changed from {change.BaseValue} to {change.TargetValue}",
|
||||
Details = $"Attribute: {change.Attribute}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version removals
|
||||
foreach (var removed in diff.Versions.DefinitionsRemoved)
|
||||
{
|
||||
if (!removed.IsBase)
|
||||
{
|
||||
breakingChanges.Add(new AbiBreakingChange
|
||||
{
|
||||
Type = AbiBreakType.VersionRemoved,
|
||||
Severity = ChangeSeverity.High,
|
||||
Symbol = removed.Name,
|
||||
Description = $"Version definition '{removed.Name}' was removed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Added exports are warnings
|
||||
foreach (var added in diff.Exports.Added)
|
||||
{
|
||||
warnings.Add(new AbiWarning
|
||||
{
|
||||
Type = AbiWarningType.SymbolAdded,
|
||||
Symbol = added.Name,
|
||||
Message = $"New exported symbol: {added.DemangledName ?? added.Name}"
|
||||
});
|
||||
}
|
||||
|
||||
// Renames are warnings
|
||||
foreach (var rename in diff.Exports.Renamed)
|
||||
{
|
||||
warnings.Add(new AbiWarning
|
||||
{
|
||||
Type = AbiWarningType.SymbolRenamed,
|
||||
Symbol = rename.BaseName,
|
||||
Message = $"Symbol renamed from '{rename.BaseDemangled ?? rename.BaseName}' to '{rename.TargetDemangled ?? rename.TargetName}'"
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate compatibility level and score
|
||||
var level = DetermineCompatibilityLevel(breakingChanges);
|
||||
var score = CalculateCompatibilityScore(diff, breakingChanges);
|
||||
|
||||
return new AbiCompatibility
|
||||
{
|
||||
Level = level,
|
||||
Score = score,
|
||||
IsBackwardCompatible = breakingChanges.Count == 0,
|
||||
IsForwardCompatible = diff.Exports.Added.Count == 0,
|
||||
BreakingChanges = breakingChanges,
|
||||
Warnings = warnings,
|
||||
Summary = new AbiSummary
|
||||
{
|
||||
TotalExportsBase = diff.Exports.Counts.TotalBase,
|
||||
TotalExportsTarget = diff.Exports.Counts.TotalTarget,
|
||||
ExportsAdded = diff.Exports.Counts.Added,
|
||||
ExportsRemoved = diff.Exports.Counts.Removed,
|
||||
ExportsModified = diff.Exports.Counts.Modified,
|
||||
BreakingChangesCount = breakingChanges.Count,
|
||||
WarningsCount = warnings.Count,
|
||||
CompatibilityPercentage = score * 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// SYM-009, SYM-010: Compute symbol changes
|
||||
private SymbolChangeSummary ComputeSymbolChanges(
|
||||
IReadOnlyList<ExtractedSymbol> baseSymbols,
|
||||
IReadOnlyList<ExtractedSymbol> targetSymbols,
|
||||
SymbolDiffOptions options)
|
||||
{
|
||||
var baseByName = baseSymbols.ToDictionary(s => s.Name, s => s);
|
||||
var targetByName = targetSymbols.ToDictionary(s => s.Name, s => s);
|
||||
|
||||
var added = new List<SymbolChange>();
|
||||
var removed = new List<SymbolChange>();
|
||||
var modified = new List<SymbolModification>();
|
||||
var renamed = new List<SymbolRename>();
|
||||
var unchanged = 0;
|
||||
|
||||
// Find added symbols
|
||||
foreach (var target in targetSymbols)
|
||||
{
|
||||
if (!baseByName.ContainsKey(target.Name))
|
||||
{
|
||||
added.Add(MapToSymbolChange(target));
|
||||
}
|
||||
}
|
||||
|
||||
// Find removed and modified symbols
|
||||
foreach (var baseSymbol in baseSymbols)
|
||||
{
|
||||
if (!targetByName.TryGetValue(baseSymbol.Name, out var targetSymbol))
|
||||
{
|
||||
removed.Add(MapToSymbolChange(baseSymbol));
|
||||
}
|
||||
else
|
||||
{
|
||||
var modification = DetectModification(baseSymbol, targetSymbol);
|
||||
if (modification is not null)
|
||||
{
|
||||
modified.Add(modification);
|
||||
}
|
||||
else
|
||||
{
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect renames (removed symbols that match added symbols via fingerprint)
|
||||
if (options.DetectRenames)
|
||||
{
|
||||
var detectedRenames = DetectRenames(
|
||||
removed,
|
||||
added,
|
||||
options.RenameSimilarityThreshold);
|
||||
|
||||
renamed.AddRange(detectedRenames);
|
||||
|
||||
// Remove renamed from added/removed
|
||||
var renamedBaseNames = new HashSet<string>(detectedRenames.Select(r => r.BaseName));
|
||||
var renamedTargetNames = new HashSet<string>(detectedRenames.Select(r => r.TargetName));
|
||||
removed.RemoveAll(r => renamedBaseNames.Contains(r.Name));
|
||||
added.RemoveAll(a => renamedTargetNames.Contains(a.Name));
|
||||
}
|
||||
|
||||
return new SymbolChangeSummary
|
||||
{
|
||||
Added = added,
|
||||
Removed = removed,
|
||||
Modified = modified,
|
||||
Renamed = renamed,
|
||||
Counts = new SymbolChangeCounts
|
||||
{
|
||||
Added = added.Count,
|
||||
Removed = removed.Count,
|
||||
Modified = modified.Count,
|
||||
Renamed = renamed.Count,
|
||||
Unchanged = unchanged,
|
||||
TotalBase = baseSymbols.Count,
|
||||
TotalTarget = targetSymbols.Count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// SYM-011: Compute version diff
|
||||
private VersionMapDiff ComputeVersionDiff(SymbolTable baseTable, SymbolTable targetTable)
|
||||
{
|
||||
var baseDefs = baseTable.VersionDefinitions.ToDictionary(v => v.Name);
|
||||
var targetDefs = targetTable.VersionDefinitions.ToDictionary(v => v.Name);
|
||||
|
||||
var defsAdded = targetTable.VersionDefinitions
|
||||
.Where(v => !baseDefs.ContainsKey(v.Name))
|
||||
.ToList();
|
||||
|
||||
var defsRemoved = baseTable.VersionDefinitions
|
||||
.Where(v => !targetDefs.ContainsKey(v.Name))
|
||||
.ToList();
|
||||
|
||||
var baseReqs = baseTable.VersionRequirements
|
||||
.ToDictionary(r => $"{r.Library}@{r.Version}");
|
||||
var targetReqs = targetTable.VersionRequirements
|
||||
.ToDictionary(r => $"{r.Library}@{r.Version}");
|
||||
|
||||
var reqsAdded = targetTable.VersionRequirements
|
||||
.Where(r => !baseReqs.ContainsKey($"{r.Library}@{r.Version}"))
|
||||
.ToList();
|
||||
|
||||
var reqsRemoved = baseTable.VersionRequirements
|
||||
.Where(r => !targetReqs.ContainsKey($"{r.Library}@{r.Version}"))
|
||||
.ToList();
|
||||
|
||||
// Detect version assignment changes
|
||||
var assignmentChanges = new List<VersionAssignmentChange>();
|
||||
var baseExports = baseTable.Exports.Where(e => e.Version is not null).ToDictionary(e => e.Name);
|
||||
|
||||
foreach (var target in targetTable.Exports.Where(e => e.Version is not null))
|
||||
{
|
||||
if (baseExports.TryGetValue(target.Name, out var baseExport))
|
||||
{
|
||||
if (baseExport.Version != target.Version)
|
||||
{
|
||||
assignmentChanges.Add(new VersionAssignmentChange
|
||||
{
|
||||
SymbolName = target.Name,
|
||||
BaseVersion = baseExport.Version,
|
||||
TargetVersion = target.Version,
|
||||
IsAbiBreaking = true // Version changes can be breaking
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new VersionMapDiff
|
||||
{
|
||||
DefinitionsAdded = defsAdded,
|
||||
DefinitionsRemoved = defsRemoved,
|
||||
RequirementsAdded = reqsAdded,
|
||||
RequirementsRemoved = reqsRemoved,
|
||||
AssignmentsChanged = assignmentChanges,
|
||||
Counts = new VersionChangeCounts
|
||||
{
|
||||
DefinitionsAdded = defsAdded.Count,
|
||||
DefinitionsRemoved = defsRemoved.Count,
|
||||
RequirementsAdded = reqsAdded.Count,
|
||||
RequirementsRemoved = reqsRemoved.Count,
|
||||
AssignmentsChanged = assignmentChanges.Count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// SYM-012: Compute dynamic linking diff
|
||||
private DynamicLinkingDiff ComputeDynamicLinkingDiff(SymbolTable baseTable, SymbolTable targetTable)
|
||||
{
|
||||
return new DynamicLinkingDiff
|
||||
{
|
||||
Got = ComputeGotDiff(baseTable.GotEntries ?? [], targetTable.GotEntries ?? []),
|
||||
Plt = ComputePltDiff(baseTable.PltEntries ?? [], targetTable.PltEntries ?? []),
|
||||
Rpath = ComputeRpathDiff(baseTable, targetTable),
|
||||
Needed = ComputeNeededDiff(baseTable.NeededLibraries, targetTable.NeededLibraries)
|
||||
};
|
||||
}
|
||||
|
||||
private GotDiff ComputeGotDiff(IReadOnlyList<GotEntry> baseEntries, IReadOnlyList<GotEntry> targetEntries)
|
||||
{
|
||||
var baseBySymbol = baseEntries.ToDictionary(e => e.SymbolName);
|
||||
var targetBySymbol = targetEntries.ToDictionary(e => e.SymbolName);
|
||||
|
||||
var added = targetEntries.Where(e => !baseBySymbol.ContainsKey(e.SymbolName)).ToList();
|
||||
var removed = baseEntries.Where(e => !targetBySymbol.ContainsKey(e.SymbolName)).ToList();
|
||||
|
||||
var modified = new List<GotEntryModification>();
|
||||
foreach (var baseEntry in baseEntries)
|
||||
{
|
||||
if (targetBySymbol.TryGetValue(baseEntry.SymbolName, out var targetEntry))
|
||||
{
|
||||
if (baseEntry.Type != targetEntry.Type || baseEntry.Address != targetEntry.Address)
|
||||
{
|
||||
modified.Add(new GotEntryModification
|
||||
{
|
||||
SymbolName = baseEntry.SymbolName,
|
||||
BaseAddress = baseEntry.Address,
|
||||
TargetAddress = targetEntry.Address,
|
||||
BaseType = baseEntry.Type,
|
||||
TargetType = targetEntry.Type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new GotDiff
|
||||
{
|
||||
EntriesAdded = added,
|
||||
EntriesRemoved = removed,
|
||||
EntriesModified = modified,
|
||||
BaseCount = baseEntries.Count,
|
||||
TargetCount = targetEntries.Count
|
||||
};
|
||||
}
|
||||
|
||||
private PltDiff ComputePltDiff(IReadOnlyList<PltEntry> baseEntries, IReadOnlyList<PltEntry> targetEntries)
|
||||
{
|
||||
var baseBySymbol = baseEntries.ToDictionary(e => e.SymbolName);
|
||||
var targetBySymbol = targetEntries.ToDictionary(e => e.SymbolName);
|
||||
|
||||
var added = targetEntries.Where(e => !baseBySymbol.ContainsKey(e.SymbolName)).ToList();
|
||||
var removed = baseEntries.Where(e => !targetBySymbol.ContainsKey(e.SymbolName)).ToList();
|
||||
|
||||
var reordered = new List<PltReorder>();
|
||||
foreach (var baseEntry in baseEntries)
|
||||
{
|
||||
if (targetBySymbol.TryGetValue(baseEntry.SymbolName, out var targetEntry))
|
||||
{
|
||||
if (baseEntry.Index != targetEntry.Index)
|
||||
{
|
||||
reordered.Add(new PltReorder
|
||||
{
|
||||
SymbolName = baseEntry.SymbolName,
|
||||
BaseIndex = baseEntry.Index,
|
||||
TargetIndex = targetEntry.Index
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new PltDiff
|
||||
{
|
||||
EntriesAdded = added,
|
||||
EntriesRemoved = removed,
|
||||
EntriesReordered = reordered,
|
||||
BaseCount = baseEntries.Count,
|
||||
TargetCount = targetEntries.Count
|
||||
};
|
||||
}
|
||||
|
||||
private RpathDiff ComputeRpathDiff(SymbolTable baseTable, SymbolTable targetTable)
|
||||
{
|
||||
var basePaths = new HashSet<string>(
|
||||
(baseTable.Rpath ?? []).Concat(baseTable.Runpath ?? []));
|
||||
var targetPaths = new HashSet<string>(
|
||||
(targetTable.Rpath ?? []).Concat(targetTable.Runpath ?? []));
|
||||
|
||||
return new RpathDiff
|
||||
{
|
||||
RpathBase = baseTable.Rpath,
|
||||
RpathTarget = targetTable.Rpath,
|
||||
RunpathBase = baseTable.Runpath,
|
||||
RunpathTarget = targetTable.Runpath,
|
||||
PathsAdded = targetPaths.Except(basePaths).ToList(),
|
||||
PathsRemoved = basePaths.Except(targetPaths).ToList(),
|
||||
HasChanges = !basePaths.SetEquals(targetPaths)
|
||||
};
|
||||
}
|
||||
|
||||
private NeededDiff ComputeNeededDiff(IReadOnlyList<string> baseLibs, IReadOnlyList<string> targetLibs)
|
||||
{
|
||||
var baseSet = new HashSet<string>(baseLibs);
|
||||
var targetSet = new HashSet<string>(targetLibs);
|
||||
|
||||
return new NeededDiff
|
||||
{
|
||||
LibrariesAdded = targetSet.Except(baseSet).ToList(),
|
||||
LibrariesRemoved = baseSet.Except(targetSet).ToList(),
|
||||
BaseLibraries = baseLibs,
|
||||
TargetLibraries = targetLibs
|
||||
};
|
||||
}
|
||||
|
||||
// SYM-013: Detect renames via fingerprint matching
|
||||
private IReadOnlyList<SymbolRename> DetectRenames(
|
||||
List<SymbolChange> removed,
|
||||
List<SymbolChange> added,
|
||||
double threshold)
|
||||
{
|
||||
var renames = new List<SymbolRename>();
|
||||
|
||||
// Only consider symbols with fingerprints
|
||||
var removedWithFp = removed.Where(r => r.Fingerprint is not null).ToList();
|
||||
var addedWithFp = added.Where(a => a.Fingerprint is not null).ToList();
|
||||
|
||||
foreach (var removedSymbol in removedWithFp)
|
||||
{
|
||||
// Find best match in added
|
||||
SymbolChange? bestMatch = null;
|
||||
double bestSimilarity = 0;
|
||||
|
||||
foreach (var addedSymbol in addedWithFp)
|
||||
{
|
||||
var similarity = ComputeFingerprintSimilarity(
|
||||
removedSymbol.Fingerprint!,
|
||||
addedSymbol.Fingerprint!);
|
||||
|
||||
if (similarity >= threshold && similarity > bestSimilarity)
|
||||
{
|
||||
bestMatch = addedSymbol;
|
||||
bestSimilarity = similarity;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch is not null)
|
||||
{
|
||||
renames.Add(new SymbolRename
|
||||
{
|
||||
BaseName = removedSymbol.Name,
|
||||
TargetName = bestMatch.Name,
|
||||
BaseDemangled = removedSymbol.DemangledName,
|
||||
TargetDemangled = bestMatch.DemangledName,
|
||||
Fingerprint = removedSymbol.Fingerprint!,
|
||||
Similarity = bestSimilarity,
|
||||
Confidence = DetermineRenameConfidence(bestSimilarity)
|
||||
});
|
||||
|
||||
// Remove matched from consideration
|
||||
addedWithFp.Remove(bestMatch);
|
||||
}
|
||||
}
|
||||
|
||||
return renames;
|
||||
}
|
||||
|
||||
// SYM-015: Compute content-addressed diff ID
|
||||
private string ComputeDiffId(
|
||||
BinaryRef baseRef,
|
||||
BinaryRef targetRef,
|
||||
SymbolChangeSummary exports,
|
||||
SymbolChangeSummary imports)
|
||||
{
|
||||
var canonical = new
|
||||
{
|
||||
base_sha256 = baseRef.Sha256,
|
||||
target_sha256 = targetRef.Sha256,
|
||||
exports_added = exports.Added.Select(e => e.Name).OrderBy(n => n, StringComparer.Ordinal),
|
||||
exports_removed = exports.Removed.Select(e => e.Name).OrderBy(n => n, StringComparer.Ordinal),
|
||||
imports_added = imports.Added.Select(i => i.Name).OrderBy(n => n, StringComparer.Ordinal),
|
||||
imports_removed = imports.Removed.Select(i => i.Name).OrderBy(n => n, StringComparer.Ordinal)
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(canonical, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false
|
||||
});
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private static SymbolChange MapToSymbolChange(ExtractedSymbol symbol)
|
||||
{
|
||||
return new SymbolChange
|
||||
{
|
||||
Name = symbol.Name,
|
||||
DemangledName = symbol.DemangledName,
|
||||
Type = symbol.Type,
|
||||
Binding = symbol.Binding,
|
||||
Visibility = symbol.Visibility,
|
||||
Section = symbol.Section,
|
||||
Address = symbol.Address,
|
||||
Size = symbol.Size,
|
||||
Version = symbol.Version,
|
||||
Fingerprint = symbol.Fingerprint
|
||||
};
|
||||
}
|
||||
|
||||
private static SymbolModification? DetectModification(ExtractedSymbol baseSymbol, ExtractedSymbol targetSymbol)
|
||||
{
|
||||
var changes = new List<AttributeChange>();
|
||||
|
||||
if (baseSymbol.Type != targetSymbol.Type)
|
||||
{
|
||||
changes.Add(new AttributeChange
|
||||
{
|
||||
Attribute = "type",
|
||||
BaseValue = baseSymbol.Type.ToString(),
|
||||
TargetValue = targetSymbol.Type.ToString(),
|
||||
Severity = ChangeSeverity.High
|
||||
});
|
||||
}
|
||||
|
||||
if (baseSymbol.Size != targetSymbol.Size)
|
||||
{
|
||||
changes.Add(new AttributeChange
|
||||
{
|
||||
Attribute = "size",
|
||||
BaseValue = baseSymbol.Size.ToString(CultureInfo.InvariantCulture),
|
||||
TargetValue = targetSymbol.Size.ToString(CultureInfo.InvariantCulture),
|
||||
Severity = targetSymbol.Size < baseSymbol.Size ? ChangeSeverity.High : ChangeSeverity.Low
|
||||
});
|
||||
}
|
||||
|
||||
if (baseSymbol.Visibility != targetSymbol.Visibility)
|
||||
{
|
||||
var severityFromVisibility = (baseSymbol.Visibility, targetSymbol.Visibility) switch
|
||||
{
|
||||
(SymbolVisibility.Default, SymbolVisibility.Hidden) => ChangeSeverity.High,
|
||||
(SymbolVisibility.Protected, SymbolVisibility.Hidden) => ChangeSeverity.High,
|
||||
_ => ChangeSeverity.Medium
|
||||
};
|
||||
|
||||
changes.Add(new AttributeChange
|
||||
{
|
||||
Attribute = "visibility",
|
||||
BaseValue = baseSymbol.Visibility.ToString(),
|
||||
TargetValue = targetSymbol.Visibility.ToString(),
|
||||
Severity = severityFromVisibility
|
||||
});
|
||||
}
|
||||
|
||||
if (baseSymbol.Binding != targetSymbol.Binding)
|
||||
{
|
||||
changes.Add(new AttributeChange
|
||||
{
|
||||
Attribute = "binding",
|
||||
BaseValue = baseSymbol.Binding.ToString(),
|
||||
TargetValue = targetSymbol.Binding.ToString(),
|
||||
Severity = ChangeSeverity.Medium
|
||||
});
|
||||
}
|
||||
|
||||
if (changes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SymbolModification
|
||||
{
|
||||
Name = baseSymbol.Name,
|
||||
DemangledName = baseSymbol.DemangledName ?? targetSymbol.DemangledName,
|
||||
Base = new SymbolAttributes
|
||||
{
|
||||
Type = baseSymbol.Type,
|
||||
Binding = baseSymbol.Binding,
|
||||
Visibility = baseSymbol.Visibility,
|
||||
Section = baseSymbol.Section,
|
||||
Address = baseSymbol.Address,
|
||||
Size = baseSymbol.Size,
|
||||
Version = baseSymbol.Version,
|
||||
Fingerprint = baseSymbol.Fingerprint
|
||||
},
|
||||
Target = new SymbolAttributes
|
||||
{
|
||||
Type = targetSymbol.Type,
|
||||
Binding = targetSymbol.Binding,
|
||||
Visibility = targetSymbol.Visibility,
|
||||
Section = targetSymbol.Section,
|
||||
Address = targetSymbol.Address,
|
||||
Size = targetSymbol.Size,
|
||||
Version = targetSymbol.Version,
|
||||
Fingerprint = targetSymbol.Fingerprint
|
||||
},
|
||||
Changes = changes,
|
||||
IsAbiBreaking = changes.Any(c => c.Severity >= ChangeSeverity.High)
|
||||
};
|
||||
}
|
||||
|
||||
private static double ComputeFingerprintSimilarity(string fp1, string fp2)
|
||||
{
|
||||
if (fp1 == fp2) return 1.0;
|
||||
|
||||
// Simple Jaccard similarity on hex characters
|
||||
var set1 = new HashSet<char>(fp1);
|
||||
var set2 = new HashSet<char>(fp2);
|
||||
var intersection = set1.Intersect(set2).Count();
|
||||
var union = set1.Union(set2).Count();
|
||||
|
||||
return union == 0 ? 0 : (double)intersection / union;
|
||||
}
|
||||
|
||||
private static RenameConfidence DetermineRenameConfidence(double similarity)
|
||||
{
|
||||
return similarity switch
|
||||
{
|
||||
>= 0.95 => RenameConfidence.VeryHigh,
|
||||
>= 0.85 => RenameConfidence.High,
|
||||
>= 0.75 => RenameConfidence.Medium,
|
||||
>= 0.65 => RenameConfidence.Low,
|
||||
_ => RenameConfidence.VeryLow
|
||||
};
|
||||
}
|
||||
|
||||
private static AbiBreakType DetermineBreakType(AttributeChange change)
|
||||
{
|
||||
return change.Attribute switch
|
||||
{
|
||||
"type" => AbiBreakType.SymbolTypeChanged,
|
||||
"size" => AbiBreakType.SymbolSizeChanged,
|
||||
"visibility" => AbiBreakType.VisibilityReduced,
|
||||
"binding" => AbiBreakType.BindingChanged,
|
||||
_ => AbiBreakType.SymbolTypeChanged
|
||||
};
|
||||
}
|
||||
|
||||
private static AbiCompatibilityLevel DetermineCompatibilityLevel(List<AbiBreakingChange> breaks)
|
||||
{
|
||||
if (breaks.Count == 0)
|
||||
{
|
||||
return AbiCompatibilityLevel.FullyCompatible;
|
||||
}
|
||||
|
||||
var criticalCount = breaks.Count(b => b.Severity == ChangeSeverity.Critical);
|
||||
var highCount = breaks.Count(b => b.Severity == ChangeSeverity.High);
|
||||
|
||||
if (criticalCount > 0 || highCount >= 10)
|
||||
{
|
||||
return AbiCompatibilityLevel.Incompatible;
|
||||
}
|
||||
|
||||
if (highCount >= 3)
|
||||
{
|
||||
return AbiCompatibilityLevel.MajorIncompatibility;
|
||||
}
|
||||
|
||||
if (highCount >= 1)
|
||||
{
|
||||
return AbiCompatibilityLevel.MinorIncompatibility;
|
||||
}
|
||||
|
||||
return AbiCompatibilityLevel.CompatibleWithWarnings;
|
||||
}
|
||||
|
||||
private static double CalculateCompatibilityScore(SymbolTableDiff diff, List<AbiBreakingChange> breaks)
|
||||
{
|
||||
if (diff.Exports.Counts.TotalBase == 0)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
var removedWeight = diff.Exports.Counts.Removed * 0.5;
|
||||
var breakingWeight = breaks.Sum(b => b.Severity switch
|
||||
{
|
||||
ChangeSeverity.Critical => 1.0,
|
||||
ChangeSeverity.High => 0.5,
|
||||
ChangeSeverity.Medium => 0.2,
|
||||
_ => 0.1
|
||||
});
|
||||
|
||||
var penalty = (removedWeight + breakingWeight) / diff.Exports.Counts.TotalBase;
|
||||
return Math.Max(0, 1.0 - penalty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for extracting symbols from binaries.
|
||||
/// </summary>
|
||||
public interface ISymbolExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts symbol table from a binary.
|
||||
/// </summary>
|
||||
Task<SymbolTable> ExtractAsync(string binaryPath, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for demangling C++/Rust names.
|
||||
/// </summary>
|
||||
public interface INameDemangler
|
||||
{
|
||||
/// <summary>
|
||||
/// Demangles a symbol name.
|
||||
/// </summary>
|
||||
string? Demangle(string mangledName);
|
||||
|
||||
/// <summary>
|
||||
/// Detects the mangling scheme.
|
||||
/// </summary>
|
||||
ManglingScheme DetectScheme(string name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name mangling scheme.
|
||||
/// </summary>
|
||||
public enum ManglingScheme
|
||||
{
|
||||
None,
|
||||
ItaniumCxx,
|
||||
MicrosoftCxx,
|
||||
Rust,
|
||||
Swift,
|
||||
Unknown
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VersionMapDiff.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Task: SYM-003 - Define VersionMapDiff records
|
||||
// Description: Version map diff model for symbol versioning changes
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
|
||||
/// <summary>
|
||||
/// Diff of symbol version maps between binaries.
|
||||
/// </summary>
|
||||
public sealed record VersionMapDiff
|
||||
{
|
||||
/// <summary>Version definitions added.</summary>
|
||||
[JsonPropertyName("definitions_added")]
|
||||
public required IReadOnlyList<VersionDefinition> DefinitionsAdded { get; init; }
|
||||
|
||||
/// <summary>Version definitions removed.</summary>
|
||||
[JsonPropertyName("definitions_removed")]
|
||||
public required IReadOnlyList<VersionDefinition> DefinitionsRemoved { get; init; }
|
||||
|
||||
/// <summary>Version requirements added.</summary>
|
||||
[JsonPropertyName("requirements_added")]
|
||||
public required IReadOnlyList<VersionRequirement> RequirementsAdded { get; init; }
|
||||
|
||||
/// <summary>Version requirements removed.</summary>
|
||||
[JsonPropertyName("requirements_removed")]
|
||||
public required IReadOnlyList<VersionRequirement> RequirementsRemoved { get; init; }
|
||||
|
||||
/// <summary>Symbol version assignments changed.</summary>
|
||||
[JsonPropertyName("assignments_changed")]
|
||||
public required IReadOnlyList<VersionAssignmentChange> AssignmentsChanged { get; init; }
|
||||
|
||||
/// <summary>Count summaries.</summary>
|
||||
[JsonPropertyName("counts")]
|
||||
public required VersionChangeCounts Counts { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A version definition (from .gnu.version_d).</summary>
|
||||
public sealed record VersionDefinition
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; init; }
|
||||
|
||||
[JsonPropertyName("flags")]
|
||||
public int Flags { get; init; }
|
||||
|
||||
[JsonPropertyName("is_base")]
|
||||
public bool IsBase { get; init; }
|
||||
|
||||
[JsonPropertyName("parent")]
|
||||
public string? Parent { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A version requirement (from .gnu.version_r).</summary>
|
||||
public sealed record VersionRequirement
|
||||
{
|
||||
[JsonPropertyName("library")]
|
||||
public required string Library { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
[JsonPropertyName("hash")]
|
||||
public uint Hash { get; init; }
|
||||
|
||||
[JsonPropertyName("flags")]
|
||||
public int Flags { get; init; }
|
||||
|
||||
[JsonPropertyName("is_weak")]
|
||||
public bool IsWeak { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A change in version assignment for a symbol.</summary>
|
||||
public sealed record VersionAssignmentChange
|
||||
{
|
||||
[JsonPropertyName("symbol_name")]
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
[JsonPropertyName("base_version")]
|
||||
public string? BaseVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("target_version")]
|
||||
public string? TargetVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("is_abi_breaking")]
|
||||
public bool IsAbiBreaking { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Count summary for version changes.</summary>
|
||||
public sealed record VersionChangeCounts
|
||||
{
|
||||
[JsonPropertyName("definitions_added")]
|
||||
public int DefinitionsAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("definitions_removed")]
|
||||
public int DefinitionsRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("requirements_added")]
|
||||
public int RequirementsAdded { get; init; }
|
||||
|
||||
[JsonPropertyName("requirements_removed")]
|
||||
public int RequirementsRemoved { get; init; }
|
||||
|
||||
[JsonPropertyName("assignments_changed")]
|
||||
public int AssignmentsChanged { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# BinaryIndex.Decompiler Module Charter
|
||||
|
||||
## Mission
|
||||
- Parse and normalize decompiler output for deterministic binary comparison.
|
||||
|
||||
## Responsibilities
|
||||
- Parse decompiled code into AST models.
|
||||
- Normalize and compare ASTs for semantic similarity.
|
||||
- Provide adapter integration for supported decompilers.
|
||||
- Maintain deterministic output and stable ordering.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/semantic-diffing.md
|
||||
|
||||
## Working Agreement
|
||||
- Deterministic parsing and normalization; avoid culture-sensitive formatting.
|
||||
- Use InvariantCulture for parsing and formatting.
|
||||
- Propagate CancellationToken for async operations.
|
||||
- Avoid random seeds unless injected and fixed.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for parser, normalization, and AST comparison.
|
||||
- Regression tests for known decompiler outputs.
|
||||
@@ -0,0 +1,27 @@
|
||||
# BinaryIndex.Ensemble Module Charter
|
||||
|
||||
## Mission
|
||||
- Combine binary analysis signals into a deterministic ensemble decision.
|
||||
|
||||
## Responsibilities
|
||||
- Aggregate semantic, decompiler, and similarity inputs.
|
||||
- Compute weighted decisions and expose results models.
|
||||
- Maintain weight tuning logic and default profiles.
|
||||
- Ensure deterministic scoring and tie-breaking.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/semantic-diffing.md
|
||||
|
||||
## Working Agreement
|
||||
- Stable ordering and deterministic weight application.
|
||||
- Use TimeProvider and IGuidGenerator for timestamps if needed.
|
||||
- Use InvariantCulture for parsing and formatting.
|
||||
- Propagate CancellationToken.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for decision engine and weight tuning.
|
||||
- Determinism tests for identical inputs.
|
||||
@@ -0,0 +1,27 @@
|
||||
# BinaryIndex.ML Module Charter
|
||||
|
||||
## Mission
|
||||
- Provide deterministic embedding and similarity services for binary analysis.
|
||||
|
||||
## Responsibilities
|
||||
- Tokenize binary code for ML inference.
|
||||
- Run ONNX inference deterministically and expose embedding APIs.
|
||||
- Maintain embedding indexes and similarity queries.
|
||||
- Support offline model loading and versioning.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/ml-model-training.md
|
||||
|
||||
## Working Agreement
|
||||
- Deterministic inference: fixed model versions and stable preprocessing.
|
||||
- Use InvariantCulture for parsing and formatting.
|
||||
- Propagate CancellationToken.
|
||||
- No network calls during inference.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for tokenizer and embedding determinism.
|
||||
- Integration tests for ONNX inference with fixed fixtures.
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.BinaryIndex.VexBridge;
|
||||
@@ -162,7 +163,7 @@ public static class BinaryMatchEvidenceSchema
|
||||
evidence[Fields.Architecture] = architecture;
|
||||
|
||||
if (resolvedAt.HasValue)
|
||||
evidence[Fields.ResolvedAt] = resolvedAt.Value.ToString("O");
|
||||
evidence[Fields.ResolvedAt] = resolvedAt.Value.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
return evidence;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# BinaryIndex Benchmarks Charter
|
||||
|
||||
## Mission
|
||||
- Maintain deterministic benchmark and accuracy tests for BinaryIndex analyzers.
|
||||
|
||||
## Responsibilities
|
||||
- Keep benchmark datasets local and fixed.
|
||||
- Ensure benchmark thresholds are stable and documented.
|
||||
- Separate benchmark runs from unit coverage.
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
|
||||
## Working Agreement
|
||||
- No network calls; offline fixtures only.
|
||||
- Fixed seeds and deterministic ordering.
|
||||
- Avoid machine-specific timing assertions; use bounded thresholds.
|
||||
|
||||
## Definition of Done
|
||||
- Benchmarks reproducible on CI and offline environments.
|
||||
@@ -12,8 +12,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="Testcontainers" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NameDemanglerTests.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Tasks: SYM-016, SYM-017 - Unit tests for name demangling
|
||||
// Description: Unit tests for C++ and Rust name demangler
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.Tests.SymbolDiff;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class NameDemanglerTests
|
||||
{
|
||||
private readonly NameDemangler _demangler = new();
|
||||
|
||||
// Scheme detection tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("_Z3foov", ManglingScheme.ItaniumCxx)]
|
||||
[InlineData("_ZN3foo3barEv", ManglingScheme.ItaniumCxx)]
|
||||
[InlineData("?foo@@YAXXZ", ManglingScheme.MicrosoftCxx)]
|
||||
[InlineData("?foo@bar@@YAXXZ", ManglingScheme.MicrosoftCxx)]
|
||||
[InlineData("_ZN4test17h0123456789abcdefE", ManglingScheme.Rust)]
|
||||
[InlineData("_RNvC5crate4main", ManglingScheme.Rust)]
|
||||
[InlineData("$s4main3fooyyF", ManglingScheme.Swift)]
|
||||
[InlineData("_$s4main3fooyyF", ManglingScheme.Swift)]
|
||||
[InlineData("foo", ManglingScheme.None)]
|
||||
[InlineData("printf", ManglingScheme.None)]
|
||||
[InlineData("", ManglingScheme.None)]
|
||||
public void DetectScheme_IdentifiesCorrectScheme(string name, ManglingScheme expected)
|
||||
{
|
||||
var result = _demangler.DetectScheme(name);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
// C++ Itanium ABI tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("_Z3foov", "foo")]
|
||||
[InlineData("_Z3bari", "bar")]
|
||||
[InlineData("_Z6myFunc", "myFunc")]
|
||||
public void Demangle_ItaniumCxx_SimpleNames(string mangled, string expected)
|
||||
{
|
||||
var result = _demangler.Demangle(mangled);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("_ZN3foo3barEv", "foo::bar")]
|
||||
[InlineData("_ZN5outer5inner4funcEv", "outer::inner::func")]
|
||||
public void Demangle_ItaniumCxx_NestedNames(string mangled, string expected)
|
||||
{
|
||||
var result = _demangler.Demangle(mangled);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
// Microsoft C++ tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("?foo@@YAXXZ", "foo")]
|
||||
[InlineData("?bar@MyClass@@QAEXXZ", "MyClass::bar")]
|
||||
public void Demangle_MicrosoftCxx_SimpleNames(string mangled, string expected)
|
||||
{
|
||||
var result = _demangler.Demangle(mangled);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
// Rust legacy mangling tests
|
||||
|
||||
[Fact]
|
||||
public void Demangle_RustLegacy_BasicName()
|
||||
{
|
||||
// _ZN<len>name...E format
|
||||
var mangled = "_ZN4test4mainE";
|
||||
var result = _demangler.Demangle(mangled);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("test", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Demangle_RustLegacy_WithHash_StripsHash()
|
||||
{
|
||||
// Rust hashes are 17h + 16 hex digits
|
||||
var mangled = "_ZN4core3ptr17h0123456789abcdefE";
|
||||
var result = _demangler.Demangle(mangled);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.DoesNotContain("h0123456789abcdef", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Demangle_RustLegacy_DecodesEscapes()
|
||||
{
|
||||
// Test Rust escape sequences
|
||||
var mangled = "_ZN4test8$LT$impl$GT$E";
|
||||
var result = _demangler.Demangle(mangled);
|
||||
|
||||
Assert.NotNull(result);
|
||||
// Should decode $LT$ to < and $GT$ to >
|
||||
Assert.Contains("<", result);
|
||||
Assert.Contains(">", result);
|
||||
}
|
||||
|
||||
// Rust v0 mangling tests
|
||||
|
||||
[Fact]
|
||||
public void Demangle_RustV0_ReturnsPlaceholder()
|
||||
{
|
||||
// Rust v0 starts with _R
|
||||
var mangled = "_RNvC5crate4main";
|
||||
var result = _demangler.Demangle(mangled);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.StartsWith("<rust-v0>", result);
|
||||
}
|
||||
|
||||
// Swift tests
|
||||
|
||||
[Fact]
|
||||
public void Demangle_Swift_ReturnsPlaceholder()
|
||||
{
|
||||
var mangled = "$s4main3fooyyF";
|
||||
var result = _demangler.Demangle(mangled);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.StartsWith("<swift>", result);
|
||||
}
|
||||
|
||||
// Edge cases
|
||||
|
||||
[Fact]
|
||||
public void Demangle_NullInput_ReturnsNull()
|
||||
{
|
||||
var result = _demangler.Demangle(null!);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Demangle_EmptyInput_ReturnsNull()
|
||||
{
|
||||
var result = _demangler.Demangle(string.Empty);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Demangle_PlainCName_ReturnsNull()
|
||||
{
|
||||
var result = _demangler.Demangle("printf");
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Demangle_InvalidMangledName_ReturnsNull()
|
||||
{
|
||||
// Invalid Itanium format (no proper length prefix)
|
||||
var result = _demangler.Demangle("_Zinvalid");
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SymbolTableDiffAnalyzerTests.cs
|
||||
// Sprint: SPRINT_20260106_001_003_BINDEX_symbol_table_diff
|
||||
// Tasks: SYM-020 to SYM-025 - Unit tests for symbol diff
|
||||
// Description: Unit tests for symbol table diff analyzer
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.BinaryIndex.Builders.SymbolDiff;
|
||||
using Xunit;
|
||||
using NSubstitute;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders.Tests.SymbolDiff;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SymbolTableDiffAnalyzerTests
|
||||
{
|
||||
private readonly ISymbolExtractor _mockExtractor;
|
||||
private readonly INameDemangler _demangler;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly SymbolTableDiffAnalyzer _analyzer;
|
||||
|
||||
public SymbolTableDiffAnalyzerTests()
|
||||
{
|
||||
_mockExtractor = Substitute.For<ISymbolExtractor>();
|
||||
_demangler = new NameDemangler();
|
||||
_timeProvider = TimeProvider.System;
|
||||
_analyzer = new SymbolTableDiffAnalyzer(
|
||||
_mockExtractor,
|
||||
_demangler,
|
||||
_timeProvider,
|
||||
NullLogger<SymbolTableDiffAnalyzer>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_DetectsAddedSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var baseTable = CreateSymbolTable("base.so", [
|
||||
CreateSymbol("foo", SymbolType.Function)
|
||||
]);
|
||||
var targetTable = CreateSymbolTable("target.so", [
|
||||
CreateSymbol("foo", SymbolType.Function),
|
||||
CreateSymbol("bar", SymbolType.Function)
|
||||
]);
|
||||
|
||||
_mockExtractor.ExtractAsync("base.so", Arg.Any<CancellationToken>()).Returns(baseTable);
|
||||
_mockExtractor.ExtractAsync("target.so", Arg.Any<CancellationToken>()).Returns(targetTable);
|
||||
|
||||
// Act
|
||||
var diff = await _analyzer.ComputeDiffAsync("base.so", "target.so");
|
||||
|
||||
// Assert
|
||||
Assert.Single(diff.Exports.Added);
|
||||
Assert.Equal("bar", diff.Exports.Added[0].Name);
|
||||
Assert.Empty(diff.Exports.Removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_DetectsRemovedSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var baseTable = CreateSymbolTable("base.so", [
|
||||
CreateSymbol("foo", SymbolType.Function),
|
||||
CreateSymbol("bar", SymbolType.Function)
|
||||
]);
|
||||
var targetTable = CreateSymbolTable("target.so", [
|
||||
CreateSymbol("foo", SymbolType.Function)
|
||||
]);
|
||||
|
||||
_mockExtractor.ExtractAsync("base.so", Arg.Any<CancellationToken>()).Returns(baseTable);
|
||||
_mockExtractor.ExtractAsync("target.so", Arg.Any<CancellationToken>()).Returns(targetTable);
|
||||
|
||||
// Act
|
||||
var diff = await _analyzer.ComputeDiffAsync("base.so", "target.so");
|
||||
|
||||
// Assert
|
||||
Assert.Empty(diff.Exports.Added);
|
||||
Assert.Single(diff.Exports.Removed);
|
||||
Assert.Equal("bar", diff.Exports.Removed[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_DetectsModifiedSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var baseTable = CreateSymbolTable("base.so", [
|
||||
CreateSymbol("foo", SymbolType.Function, size: 100)
|
||||
]);
|
||||
var targetTable = CreateSymbolTable("target.so", [
|
||||
CreateSymbol("foo", SymbolType.Function, size: 200)
|
||||
]);
|
||||
|
||||
_mockExtractor.ExtractAsync("base.so", Arg.Any<CancellationToken>()).Returns(baseTable);
|
||||
_mockExtractor.ExtractAsync("target.so", Arg.Any<CancellationToken>()).Returns(targetTable);
|
||||
|
||||
// Act
|
||||
var diff = await _analyzer.ComputeDiffAsync("base.so", "target.so");
|
||||
|
||||
// Assert
|
||||
Assert.Single(diff.Exports.Modified);
|
||||
Assert.Equal("foo", diff.Exports.Modified[0].Name);
|
||||
Assert.Contains(diff.Exports.Modified[0].Changes, c => c.Attribute == "size");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_DetectsRenames_WhenFingerprintsMatch()
|
||||
{
|
||||
// Arrange
|
||||
var fingerprint = "abc123def456";
|
||||
var baseTable = CreateSymbolTable("base.so", [
|
||||
CreateSymbol("old_name", SymbolType.Function, fingerprint: fingerprint)
|
||||
]);
|
||||
var targetTable = CreateSymbolTable("target.so", [
|
||||
CreateSymbol("new_name", SymbolType.Function, fingerprint: fingerprint)
|
||||
]);
|
||||
|
||||
_mockExtractor.ExtractAsync("base.so", Arg.Any<CancellationToken>()).Returns(baseTable);
|
||||
_mockExtractor.ExtractAsync("target.so", Arg.Any<CancellationToken>()).Returns(targetTable);
|
||||
|
||||
// Act
|
||||
var diff = await _analyzer.ComputeDiffAsync("base.so", "target.so", new SymbolDiffOptions
|
||||
{
|
||||
DetectRenames = true,
|
||||
RenameSimilarityThreshold = 0.5
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Single(diff.Exports.Renamed);
|
||||
Assert.Equal("old_name", diff.Exports.Renamed[0].BaseName);
|
||||
Assert.Equal("new_name", diff.Exports.Renamed[0].TargetName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComputeDiffAsync_ComputesDiffId_Deterministically()
|
||||
{
|
||||
// Arrange
|
||||
var baseTable = CreateSymbolTable("base.so", [
|
||||
CreateSymbol("foo", SymbolType.Function)
|
||||
]);
|
||||
var targetTable = CreateSymbolTable("target.so", [
|
||||
CreateSymbol("foo", SymbolType.Function),
|
||||
CreateSymbol("bar", SymbolType.Function)
|
||||
]);
|
||||
|
||||
_mockExtractor.ExtractAsync("base.so", Arg.Any<CancellationToken>()).Returns(baseTable);
|
||||
_mockExtractor.ExtractAsync("target.so", Arg.Any<CancellationToken>()).Returns(targetTable);
|
||||
|
||||
// Act
|
||||
var diff1 = await _analyzer.ComputeDiffAsync("base.so", "target.so");
|
||||
var diff2 = await _analyzer.ComputeDiffAsync("base.so", "target.so");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(diff1.DiffId, diff2.DiffId);
|
||||
Assert.StartsWith("sha256:", diff1.DiffId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssessAbiCompatibility_FullyCompatible_WhenNoBreakingChanges()
|
||||
{
|
||||
// Arrange
|
||||
var diff = CreateDiff(
|
||||
added: [CreateSymbolChange("new_func")],
|
||||
removed: [],
|
||||
modified: []);
|
||||
|
||||
// Act
|
||||
var abi = _analyzer.AssessAbiCompatibility(diff);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AbiCompatibilityLevel.FullyCompatible, abi.Level);
|
||||
Assert.True(abi.IsBackwardCompatible);
|
||||
Assert.Empty(abi.BreakingChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssessAbiCompatibility_Incompatible_WhenSymbolsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
var diff = CreateDiff(
|
||||
added: [],
|
||||
removed: [CreateSymbolChange("removed_func", SymbolBinding.Global)],
|
||||
modified: []);
|
||||
|
||||
// Act
|
||||
var abi = _analyzer.AssessAbiCompatibility(diff);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(AbiCompatibilityLevel.FullyCompatible, abi.Level);
|
||||
Assert.False(abi.IsBackwardCompatible);
|
||||
Assert.Single(abi.BreakingChanges);
|
||||
Assert.Equal(AbiBreakType.SymbolRemoved, abi.BreakingChanges[0].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssessAbiCompatibility_WarningsForAddedSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var diff = CreateDiff(
|
||||
added: [CreateSymbolChange("new_func")],
|
||||
removed: [],
|
||||
modified: []);
|
||||
|
||||
// Act
|
||||
var abi = _analyzer.AssessAbiCompatibility(diff);
|
||||
|
||||
// Assert
|
||||
Assert.Single(abi.Warnings);
|
||||
Assert.Equal(AbiWarningType.SymbolAdded, abi.Warnings[0].Type);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private static SymbolTable CreateSymbolTable(string path, IReadOnlyList<ExtractedSymbol> exports)
|
||||
{
|
||||
return new SymbolTable
|
||||
{
|
||||
Binary = new BinaryRef
|
||||
{
|
||||
Path = path,
|
||||
Sha256 = $"sha256:{Guid.NewGuid():N}",
|
||||
Architecture = "x86_64",
|
||||
Format = BinaryFormat.Elf
|
||||
},
|
||||
Exports = exports,
|
||||
Imports = [],
|
||||
VersionDefinitions = [],
|
||||
VersionRequirements = [],
|
||||
NeededLibraries = [],
|
||||
ExtractedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static ExtractedSymbol CreateSymbol(
|
||||
string name,
|
||||
SymbolType type,
|
||||
SymbolBinding binding = SymbolBinding.Global,
|
||||
ulong size = 64,
|
||||
string? fingerprint = null)
|
||||
{
|
||||
return new ExtractedSymbol
|
||||
{
|
||||
Name = name,
|
||||
Type = type,
|
||||
Binding = binding,
|
||||
Visibility = SymbolVisibility.Default,
|
||||
Address = 0x1000,
|
||||
Size = size,
|
||||
Fingerprint = fingerprint
|
||||
};
|
||||
}
|
||||
|
||||
private static SymbolChange CreateSymbolChange(
|
||||
string name,
|
||||
SymbolBinding binding = SymbolBinding.Global)
|
||||
{
|
||||
return new SymbolChange
|
||||
{
|
||||
Name = name,
|
||||
Type = SymbolType.Function,
|
||||
Binding = binding,
|
||||
Visibility = SymbolVisibility.Default,
|
||||
Address = 0x1000,
|
||||
Size = 64
|
||||
};
|
||||
}
|
||||
|
||||
private static SymbolTableDiff CreateDiff(
|
||||
IReadOnlyList<SymbolChange> added,
|
||||
IReadOnlyList<SymbolChange> removed,
|
||||
IReadOnlyList<SymbolModification> modified)
|
||||
{
|
||||
return new SymbolTableDiff
|
||||
{
|
||||
DiffId = "sha256:test",
|
||||
Base = new BinaryRef
|
||||
{
|
||||
Path = "base.so",
|
||||
Sha256 = "sha256:base",
|
||||
Architecture = "x86_64",
|
||||
Format = BinaryFormat.Elf
|
||||
},
|
||||
Target = new BinaryRef
|
||||
{
|
||||
Path = "target.so",
|
||||
Sha256 = "sha256:target",
|
||||
Architecture = "x86_64",
|
||||
Format = BinaryFormat.Elf
|
||||
},
|
||||
Exports = new SymbolChangeSummary
|
||||
{
|
||||
Added = added,
|
||||
Removed = removed,
|
||||
Modified = modified,
|
||||
Renamed = [],
|
||||
Counts = new SymbolChangeCounts
|
||||
{
|
||||
Added = added.Count,
|
||||
Removed = removed.Count,
|
||||
Modified = modified.Count,
|
||||
TotalBase = removed.Count + modified.Count,
|
||||
TotalTarget = added.Count + modified.Count
|
||||
}
|
||||
},
|
||||
Imports = new SymbolChangeSummary
|
||||
{
|
||||
Added = [],
|
||||
Removed = [],
|
||||
Modified = [],
|
||||
Renamed = [],
|
||||
Counts = new SymbolChangeCounts()
|
||||
},
|
||||
Versions = new VersionMapDiff
|
||||
{
|
||||
DefinitionsAdded = [],
|
||||
DefinitionsRemoved = [],
|
||||
RequirementsAdded = [],
|
||||
RequirementsRemoved = [],
|
||||
AssignmentsChanged = [],
|
||||
Counts = new VersionChangeCounts()
|
||||
},
|
||||
AbiCompatibility = new AbiCompatibility
|
||||
{
|
||||
Level = AbiCompatibilityLevel.FullyCompatible,
|
||||
Score = 1.0,
|
||||
IsBackwardCompatible = true,
|
||||
IsForwardCompatible = true,
|
||||
BreakingChanges = [],
|
||||
Warnings = [],
|
||||
Summary = new AbiSummary()
|
||||
},
|
||||
ComputedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# BinaryIndex.Decompiler Tests Charter
|
||||
|
||||
## Mission
|
||||
- Validate deterministic decompiler parsing and normalization.
|
||||
|
||||
## Responsibilities
|
||||
- Cover AST parsing, normalization, and comparison paths.
|
||||
- Keep fixtures deterministic and offline-safe.
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/semantic-diffing.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
|
||||
## Definition of Done
|
||||
- Tests are deterministic and offline-safe.
|
||||
- Coverage includes error handling and normalization edge cases.
|
||||
|
||||
## Working Agreement
|
||||
- Use fixed seeds and ids in fixtures.
|
||||
- Avoid non-deterministic ordering; assert sorted output.
|
||||
@@ -20,9 +20,6 @@
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -18,13 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# BinaryIndex.Ensemble Tests Charter
|
||||
|
||||
## Mission
|
||||
- Validate deterministic ensemble decisioning and weight tuning.
|
||||
|
||||
## Responsibilities
|
||||
- Cover decision engine inputs, weights, and tie-breaking.
|
||||
- Keep fixtures deterministic and offline-safe.
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/semantic-diffing.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
|
||||
## Definition of Done
|
||||
- Tests are deterministic and offline-safe.
|
||||
- Coverage includes weight tuning and error handling.
|
||||
|
||||
## Working Agreement
|
||||
- Use fixed seeds and ids in fixtures.
|
||||
- Avoid non-deterministic ordering; assert sorted output.
|
||||
@@ -0,0 +1,22 @@
|
||||
# BinaryIndex.Ghidra Tests Charter
|
||||
|
||||
## Mission
|
||||
- Validate deterministic behavior of the Ghidra integration layer.
|
||||
|
||||
## Responsibilities
|
||||
- Cover service behaviors, process lifecycle, and output parsing.
|
||||
- Keep fixtures deterministic and offline-safe.
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/ghidra-deployment.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
|
||||
## Definition of Done
|
||||
- Tests are deterministic and offline-safe.
|
||||
- Coverage includes error handling and cleanup paths.
|
||||
|
||||
## Working Agreement
|
||||
- Use fixed ids and temp paths in fixtures.
|
||||
- Avoid non-deterministic ordering; assert sorted output.
|
||||
@@ -16,13 +16,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# BinaryIndex.Semantic Tests Charter
|
||||
|
||||
## Mission
|
||||
- Validate deterministic semantic graph extraction and matching.
|
||||
|
||||
## Responsibilities
|
||||
- Cover graph extraction, hashing, canonicalization, and matching.
|
||||
- Keep fixtures deterministic and offline-safe.
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/binary-index/architecture.md
|
||||
- docs/modules/binary-index/semantic-diffing.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
|
||||
## Definition of Done
|
||||
- Tests are deterministic and offline-safe.
|
||||
- Coverage includes algorithm options and edge cases.
|
||||
|
||||
## Working Agreement
|
||||
- Use fixed seeds and ids in fixtures.
|
||||
- Avoid non-deterministic ordering; assert sorted output.
|
||||
@@ -15,10 +15,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user