using System; using System.Text; using StellaOps.Cryptography; namespace StellaOps.Scanner.Reachability; /// /// Builds canonical CodeIDs with compliance-profile-aware hashing. /// Uses which resolves to: /// - SHA-256 for "world" and "fips" profiles /// - GOST3411-2012-256 for "gost" profile /// - SM3 for "sm" profile /// /// /// Format: code:{lang}:{base64url-hash} where the hash is computed over a /// canonical tuple that is stable across machines and paths. /// public sealed class CodeIdBuilder { private readonly ICryptoHash _cryptoHash; /// /// Creates a new CodeIdBuilder with the specified crypto hash service. /// /// Crypto hash service for compliance-aware hashing. public CodeIdBuilder(ICryptoHash cryptoHash) { _cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash)); } /// /// Creates a binary code-id from binary components. /// public string ForBinary(string buildId, string section, string? relativePath) { var tuple = $"{Norm(buildId)}\0{Norm(section)}\0{Norm(relativePath)}"; return Build("binary", tuple); } /// /// Creates a .NET code-id from assembly components. /// public string ForDotNet(string assemblyName, string moduleName, string? mvid) { var tuple = $"{Norm(assemblyName)}\0{Norm(moduleName)}\0{Norm(mvid)}"; return Build("dotnet", tuple); } /// /// Creates a binary code-id using canonical address + length tuple. /// public string ForBinarySegment(string format, string fileHash, string address, long? lengthBytes = null, string? section = null, string? codeBlockHash = null) { var tuple = $"{Norm(format)}\0{Norm(fileHash)}\0{NormalizeAddress(address)}\0{NormalizeLength(lengthBytes)}\0{Norm(section)}\0{Norm(codeBlockHash)}"; return Build("binary", tuple); } /// /// Creates a Node code-id from package components. /// public string ForNode(string packageName, string entryPath) { var tuple = $"{Norm(packageName)}\0{Norm(entryPath)}"; return Build("node", tuple); } /// /// Creates a code-id from an existing symbol ID. /// public string FromSymbolId(string symbolId) { ArgumentException.ThrowIfNullOrWhiteSpace(symbolId); return Build("sym", symbolId.Trim()); } private string Build(string lang, string tuple) { var bytes = Encoding.UTF8.GetBytes(tuple); var hash = _cryptoHash.ComputeHashForPurpose(bytes, HashPurpose.Symbol); var base64 = Convert.ToBase64String(hash) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); return $"code:{lang}:{base64}"; } private static string NormalizeAddress(string? value) { if (string.IsNullOrWhiteSpace(value)) { return "0x0"; } var addrText = value.Trim(); var isHex = addrText.StartsWith("0x", StringComparison.OrdinalIgnoreCase); if (isHex) { addrText = addrText[2..]; } if (long.TryParse(addrText, isHex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var addrValue)) { if (addrValue < 0) { addrValue = 0; } return $"0x{addrValue:x}"; } addrText = addrText.TrimStart('0'); if (addrText.Length == 0) { addrText = "0"; } return $"0x{addrText.ToLowerInvariant()}"; } private static string NormalizeLength(long? value) { if (value is null or <= 0) { return "unknown"; } return value.Value.ToString("D", System.Globalization.CultureInfo.InvariantCulture); } private static string Norm(string? value) => (value ?? string.Empty).Trim(); }