audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -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; }
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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; }
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -12,8 +12,8 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="Testcontainers" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>

View File

@@ -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);
}
}

View File

@@ -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
};
}
}

View File

@@ -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.

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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.

View File

@@ -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.

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>