Phase 2 WP-1–13+23: Template Engine CRUD, composition, overrides, locking, collision detection, acyclicity
- WP-23: ITemplateEngineRepository full EF Core implementation - WP-1: Template CRUD with deletion constraints (instances, children, compositions) - WP-2–4: Attribute, alarm, script definitions with lock flags and override granularity - WP-5: Shared script CRUD with syntax validation - WP-6–7: Composition with recursive nesting and canonical naming - WP-8–11: Override granularity, locking rules, inheritance/composition scope - WP-12: Naming collision detection on canonical names (recursive) - WP-13: Graph acyclicity (inheritance + composition cycles) Core services: TemplateService, SharedScriptService, TemplateResolver, LockEnforcer, CollisionDetector, CycleDetector. 358 tests pass.
This commit is contained in:
120
src/ScadaLink.TemplateEngine/Validation/ScriptCompiler.cs
Normal file
120
src/ScadaLink.TemplateEngine/Validation/ScriptCompiler.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.TemplateEngine.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validates script code by attempting to compile it using Roslyn.
|
||||
/// In production, this would compile C# scripts against a stub ScriptApi assembly
|
||||
/// that provides the allowed API surface (attribute read/write, CallScript, CallShared, etc.)
|
||||
/// and enforces the forbidden API list (System.IO, Process, Threading, Reflection, raw network).
|
||||
///
|
||||
/// For now, this implementation performs basic syntax validation.
|
||||
/// </summary>
|
||||
public class ScriptCompiler
|
||||
{
|
||||
// Forbidden namespace patterns - scripts must not use these
|
||||
private static readonly string[] ForbiddenPatterns =
|
||||
[
|
||||
"System.IO.",
|
||||
"System.Diagnostics.Process",
|
||||
"System.Threading.",
|
||||
"System.Reflection.",
|
||||
"System.Net.Sockets.",
|
||||
"System.Net.Http.",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to compile a script and returns success or a compilation error.
|
||||
/// </summary>
|
||||
/// <param name="code">The C# script code.</param>
|
||||
/// <param name="scriptName">The canonical name of the script (for error messages).</param>
|
||||
/// <returns>Success if the script compiles, or Failure with the error message.</returns>
|
||||
public Result<bool> TryCompile(string code, string scriptName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return Result<bool>.Failure($"Script '{scriptName}' has empty code.");
|
||||
|
||||
// Check for forbidden APIs
|
||||
foreach (var pattern in ForbiddenPatterns)
|
||||
{
|
||||
if (code.Contains(pattern, StringComparison.Ordinal))
|
||||
{
|
||||
return Result<bool>.Failure(
|
||||
$"Script '{scriptName}' uses forbidden API: '{pattern.TrimEnd('.')}'. " +
|
||||
"Scripts cannot use System.IO, Process, Threading, Reflection, or raw network APIs.");
|
||||
}
|
||||
}
|
||||
|
||||
// Basic brace matching validation
|
||||
var braceDepth = 0;
|
||||
var inString = false;
|
||||
var inLineComment = false;
|
||||
var inBlockComment = false;
|
||||
|
||||
for (int i = 0; i < code.Length; i++)
|
||||
{
|
||||
var c = code[i];
|
||||
var next = i + 1 < code.Length ? code[i + 1] : '\0';
|
||||
|
||||
if (inLineComment)
|
||||
{
|
||||
if (c == '\n') inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment)
|
||||
{
|
||||
if (c == '*' && next == '/')
|
||||
{
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '/')
|
||||
{
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '*')
|
||||
{
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' && !inString)
|
||||
{
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' && inString)
|
||||
{
|
||||
// Check for escaped quote
|
||||
if (i > 0 && code[i - 1] != '\\')
|
||||
inString = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) continue;
|
||||
|
||||
if (c == '{') braceDepth++;
|
||||
else if (c == '}') braceDepth--;
|
||||
|
||||
if (braceDepth < 0)
|
||||
return Result<bool>.Failure($"Script '{scriptName}' has mismatched braces (unexpected closing brace).");
|
||||
}
|
||||
|
||||
if (braceDepth != 0)
|
||||
return Result<bool>.Failure($"Script '{scriptName}' has mismatched braces ({braceDepth} unclosed).");
|
||||
|
||||
if (inBlockComment)
|
||||
return Result<bool>.Failure($"Script '{scriptName}' has an unclosed block comment.");
|
||||
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user