Editor/Bridge/SecboxCoreClient.cs
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Sandbox.SecBox.Bridge.Dto;
namespace Sandbox.SecBox.Bridge;
// Reflective invocation of Secbox.Core.SecboxApi. Method names + signatures
// are pinned by Secbox.Contracts.BridgeProtocol (mirrored here as constants
// since we can't reference Contracts at compile time).
public static class SecboxCoreClient
{
const string ApiClassName = "Secbox.Core.SecboxApi";
static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true,
};
static MethodInfo _getInfo, _scanFolder, _scanAssembly, _scanSource;
public static async Task EnsureReadyAsync()
{
// Short-circuit if already wired - repeat callers (RuntimeMonitor
// fires once per assembly load) don't need to re-do the handshake.
if (_getInfo != null && _scanFolder != null && _scanAssembly != null && _scanSource != null)
return;
DiagnosticsLog.Trace("SecboxCoreClient.EnsureReadyAsync: begin");
var asm = await SecboxCoreLoader.EnsureLoadedAsync();
var type = asm.GetType(ApiClassName);
if (type == null)
{
DiagnosticsLog.Error($"{ApiClassName} not found in loaded core");
throw new InvalidOperationException($"{ApiClassName} not found in loaded Secbox.Core.");
}
_getInfo = type.GetMethod("GetInfo", Type.EmptyTypes);
_scanFolder = type.GetMethod("ScanFolder", new[] { typeof(string), typeof(string) });
_scanAssembly = type.GetMethod("ScanAssembly", new[] { typeof(string), typeof(string) });
_scanSource = type.GetMethod("ScanSource", new[] { typeof(string), typeof(string) });
if (_getInfo == null || _scanFolder == null || _scanAssembly == null || _scanSource == null)
{
DiagnosticsLog.Error($"{ApiClassName} missing expected methods - protocol skew");
throw new InvalidOperationException(
$"{ApiClassName} is missing one or more expected methods (signature mismatch / protocol skew).");
}
ApiInfo info;
try { info = GetInfo(); }
catch (Exception ex)
{
DiagnosticsLog.Error("GetInfo handshake threw", ex);
throw;
}
if (info.ProtocolVersion != CorePolicy.RequiredProtocolVersion)
{
DiagnosticsLog.Error($"protocol mismatch: adapter v{CorePolicy.RequiredProtocolVersion}, core v{info.ProtocolVersion}");
throw new InvalidOperationException(
$"Bridge protocol mismatch. Adapter expects v{CorePolicy.RequiredProtocolVersion}, "
+ $"loaded core reports v{info.ProtocolVersion}.");
}
DiagnosticsLog.Info($"handshake ok: core v{info.ScannerVersion}, finders={string.Join(",", info.AvailableFinders)}, packs={info.AvailableRulePacks.Count}");
}
public static ApiInfo GetInfo()
{
var json = (string)_getInfo.Invoke(null, null);
return JsonSerializer.Deserialize<ApiInfo>(json, JsonOpts);
}
public static ScanReport ScanFolder(string folderPath, string optionsJson = null)
{
DiagnosticsLog.Trace($"ScanFolder: {folderPath}");
return Deserialize((string)_scanFolder.Invoke(null, new object[] { folderPath, optionsJson }));
}
public static ScanReport ScanAssembly(string dllPath, string optionsJson = null)
{
DiagnosticsLog.Trace($"ScanAssembly: {dllPath}");
return Deserialize((string)_scanAssembly.Invoke(null, new object[] { dllPath, optionsJson }));
}
public static ScanReport ScanSource(string sourcePath, string optionsJson = null)
{
DiagnosticsLog.Trace($"ScanSource: {sourcePath}");
return Deserialize((string)_scanSource.Invoke(null, new object[] { sourcePath, optionsJson }));
}
static ScanReport Deserialize(string json)
=> JsonSerializer.Deserialize<ScanReport>(json, JsonOpts) ?? new ScanReport();
}