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