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