using ScadaLink.Commons.Types; namespace ScadaLink.TemplateEngine.Validation; /// /// 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. /// public class ScriptCompiler { /// /// Forbidden namespace patterns — scripts (and trigger expressions, via /// ) must not use these. Trigger expressions run /// under the same trust model as scripts, so the list is shared from here rather /// than duplicated. /// internal static readonly string[] ForbiddenPatterns = [ "System.IO.", "System.Diagnostics.Process", "System.Threading.", "System.Reflection.", "System.Net.Sockets.", "System.Net.Http.", ]; /// /// Attempts to compile a script and returns success or a compilation error. /// /// The C# script code. /// The canonical name of the script (for error messages). /// Success if the script compiles, or Failure with the error message. public Result TryCompile(string code, string scriptName) { if (string.IsNullOrWhiteSpace(code)) return Result.Failure($"Script '{scriptName}' has empty code."); // Check for forbidden APIs foreach (var pattern in ForbiddenPatterns) { if (code.Contains(pattern, StringComparison.Ordinal)) { return Result.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.Failure($"Script '{scriptName}' has mismatched braces (unexpected closing brace)."); } if (braceDepth != 0) return Result.Failure($"Script '{scriptName}' has mismatched braces ({braceDepth} unclosed)."); if (inBlockComment) return Result.Failure($"Script '{scriptName}' has an unclosed block comment."); return Result.Success(true); } }