126 lines
4.0 KiB
C#
126 lines
4.0 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Forbidden namespace patterns — scripts (and trigger expressions, via
|
|
/// <see cref="ValidationService"/>) must not use these. Trigger expressions run
|
|
/// under the same trust model as scripts, so the list is shared from here rather
|
|
/// than duplicated.
|
|
/// </summary>
|
|
internal 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);
|
|
}
|
|
}
|