using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
///
/// Process-wide cache of script compile verdicts, keyed on the pair
/// (globals surface, SHA-256 hash of the script code). The verdict (clean/compiles,
/// or the name-free error text) is a pure function of the code plus the
/// compile-time-static ScriptTrustPolicy AND the globals surface it was
/// compiled against, so it is sound to memoise it across the whole process: an
/// unchanged script runs the forbidden-API analysis and the Roslyn compile exactly
/// once, removing both the repeat CPU cost and the repeat compiled-assembly load.
///
///
/// The globals surface is part of the key because a verdict is NOT interchangeable
/// across surfaces: a trigger expression that compiles clean against
/// TriggerCompileSurface is not vetted for a ScriptCompileSurface
/// script body (different globals resolve different identifiers), so a code-only key
/// could return a stale "clean" for code never compiled against the caller's surface.
/// Keeping the surface in the key makes that cross-surface reuse structurally
/// impossible even as more compile surfaces gain cache entries.
///
///
///
/// The stored error is deliberately name-free — the caller's script name is
/// formatted into the returned message at read time, so two callers submitting the
/// same bad code each see their own name in the failure text while sharing one verdict.
///
///
///
/// Bounded at entries; on overflow the cache is simply
/// cleared (the verdict is cheap to recompute, so a coarse reset is safe and avoids
/// eviction bookkeeping). //
/// are exposed for tests and diagnostics.
///
///
public static class ScriptCompileVerdictCache
{
/// Upper bound on cached entries; the cache is cleared wholesale on overflow.
private const int MaxEntries = 4096;
private static readonly ConcurrentDictionary Cache = new();
private static long _hits;
/// Number of cache hits observed since the last .
public static long Hits => Interlocked.Read(ref _hits);
/// Current number of cached verdicts.
public static int Count => Cache.Count;
///
/// Returns the cached verdict for the pair (,
/// ), or computes it via and
/// caches it on a miss. A hit increments . The verdict's error
/// text must be name-free — the caller formats the script name in on return.
///
///
/// The globals-surface discriminator (e.g. nameof(ScriptCompileSurface) or
/// nameof(TriggerCompileSurface)). Part of the key so a verdict computed
/// against one surface is never reused for another.
///
/// The script source code to look up (hashed to form the cache key).
/// Computes the verdict on a cache miss.
/// The cached or newly computed compile verdict.
public static (bool Ok, string? Error) GetOrAdd(string surface, string code, Func<(bool Ok, string? Error)> factory)
{
var key = surface + ":" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(code)));
if (Cache.TryGetValue(key, out var verdict))
{
Interlocked.Increment(ref _hits);
return verdict;
}
verdict = factory();
// Coarse bound: on overflow drop everything rather than track evictions.
if (Cache.Count >= MaxEntries)
Cache.Clear();
Cache[key] = verdict;
return verdict;
}
/// Clears all cached verdicts and resets the hit counter.
public static void Clear()
{
Cache.Clear();
Interlocked.Exchange(ref _hits, 0);
}
}