5a878b78d4
Add missing <summary>/<param>/<returns>/<typeparam> tags and switch interface implementations to <inheritdoc/> across 106 files; strip project bookkeeping identifiers (Task NN, #05-TNN, PLAN-04, StoreAndForward-0NN) from shipped code comments while preserving the descriptive rationale. Comment-only: zero code-logic lines changed; solution builds 0/0. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
78 lines
3.2 KiB
C#
78 lines
3.2 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
|
|
|
/// <summary>
|
|
/// Process-wide cache of script compile verdicts, keyed on a 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 <c>ScriptTrustPolicy</c> and
|
|
/// globals surface, 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.
|
|
///
|
|
/// <para>
|
|
/// The stored error is deliberately <em>name-free</em> — 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.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Bounded at <see cref="MaxEntries"/> entries; on overflow the cache is simply
|
|
/// cleared (the verdict is cheap to recompute, so a coarse reset is safe and avoids
|
|
/// eviction bookkeeping). <see cref="Hits"/>/<see cref="Count"/>/<see cref="Clear"/>
|
|
/// are exposed for tests and diagnostics.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class ScriptCompileVerdictCache
|
|
{
|
|
/// <summary>Upper bound on cached entries; the cache is cleared wholesale on overflow.</summary>
|
|
private const int MaxEntries = 4096;
|
|
|
|
private static readonly ConcurrentDictionary<string, (bool Ok, string? Error)> Cache = new();
|
|
private static long _hits;
|
|
|
|
/// <summary>Number of cache hits observed since the last <see cref="Clear"/>.</summary>
|
|
public static long Hits => Interlocked.Read(ref _hits);
|
|
|
|
/// <summary>Current number of cached verdicts.</summary>
|
|
public static int Count => Cache.Count;
|
|
|
|
/// <summary>
|
|
/// Returns the cached verdict for <paramref name="code"/>, or computes it via
|
|
/// <paramref name="factory"/> and caches it on a miss. A hit increments
|
|
/// <see cref="Hits"/>. The verdict's error text must be name-free — the caller
|
|
/// formats the script name in on return.
|
|
/// </summary>
|
|
/// <param name="code">The script source code to look up (hashed to form the cache key).</param>
|
|
/// <param name="factory">Computes the verdict on a cache miss.</param>
|
|
/// <returns>The cached or newly computed compile verdict.</returns>
|
|
public static (bool Ok, string? Error) GetOrAdd(string code, Func<(bool Ok, string? Error)> factory)
|
|
{
|
|
var key = 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;
|
|
}
|
|
|
|
/// <summary>Clears all cached verdicts and resets the hit counter.</summary>
|
|
public static void Clear()
|
|
{
|
|
Cache.Clear();
|
|
Interlocked.Exchange(ref _hits, 0);
|
|
}
|
|
}
|