Single commit covering the four small/medium fixes from the updated code review. Core.Scripting-014 (Medium, Concurrency): CompiledScriptCache.Clear() used the key-only TryRemove(key, out var lazy) overload — same race shape Core.Scripting-006 closed in GetOrCompile's catch block. A concurrent re-add between snapshot and TryRemove was evicted + disposed while the new caller still held it. Replaced with the value-scoped TryRemove(KeyValuePair<,>) overload. Regression test Clear_uses_value_scoped_TryRemove_so_a_race_inserted_entry_survives added. Core.Scripting-013 (Medium, Security): Hand-rolled BuildWrapperSource pastes user source between literal braces; brace-balanced source could inject sibling methods/classes alongside CompiledScript.Run. Analyzer still walked the injected members so it wasn't a direct escape, but it relaxed the documented 'method body' authoring contract. Added EnforceSingleRunMember: after ParseText, the compilation unit must hold exactly one type (CompiledScript) and that type must hold exactly one member (the Run method). Any deviation throws CompilationErrorException with LMX001/ LMX002 diagnostic IDs and a Core.Scripting-013 reference in the message. Two regression tests added covering the sibling-method and sibling-class injection vectors. Core.Scripting-015 (Low, Correctness, latent): ToCSharpTypeName's generic branch truncated at the first backtick via IndexOf, silently dropping closed args of nested-generic shapes (Outer<T>.Inner<U>). No production caller exercises this shape today (all TContext/TResult are top-level non-nested), so the bug was latent. Rewrote the generic branch to walk the FullName segment-by- segment, consuming generic args per segment so nested shapes emit valid C# (global::Ns.Outer<T>.Inner<U> rather than the broken Outer<T,U>). Core.ScriptedAlarms-013 (Low, Documentation): The internal test accessors TryGetScratchReadCacheForTest / TryGetScratchContextForTest return live mutable scratch refilled in place under _evalGate. XML docs didn't warn future test authors about the synchronization contract. Added a <remarks> block to each documenting the only-safe-on-quiesced-engine + identity-or-single-key contract. Verification (suites green): Core.Scripting.Tests: 110/110 (was 107 — +3 new rejection/race tests) Core.ScriptedAlarms.Tests: 67/67 (unchanged — doc-only fix) Core.VirtualTags.Tests: 57/57 (unchanged) After this commit, all 12 findings from the updated re-review are closed (10 Resolved, 1 Won't Fix none, 1 Deferred — Driver.Galaxy-017). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
534 lines
21 KiB
C#
534 lines
21 KiB
C#
using Microsoft.CodeAnalysis.Scripting;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
|
|
|
|
/// <summary>
|
|
/// Compiles scripts against the Phase 7 sandbox + asserts every forbidden API
|
|
/// (HttpClient / File / Process / reflection) fails at compile, not at evaluation.
|
|
/// Locks decision #6 — scripts can't escape to the broader .NET surface.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ScriptSandboxTests
|
|
{
|
|
[Fact]
|
|
public void Happy_path_script_compiles_and_returns()
|
|
{
|
|
// Baseline — ctx + Math + basic types must work.
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, double>.Compile(
|
|
"""
|
|
var v = (double)ctx.GetTag("X").Value;
|
|
return Math.Abs(v) * 2.0;
|
|
""");
|
|
evaluator.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Happy_path_script_runs_and_reads_seeded_tag()
|
|
{
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, double>.Compile(
|
|
"""return (double)ctx.GetTag("In").Value * 2.0;""");
|
|
|
|
var ctx = new FakeScriptContext().Seed("In", 21.0);
|
|
var result = await evaluator.RunAsync(ctx, TestContext.Current.CancellationToken);
|
|
result.ShouldBe(42.0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetVirtualTag_records_the_write()
|
|
{
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
ctx.SetVirtualTag("Out", 42);
|
|
return 0;
|
|
""");
|
|
var ctx = new FakeScriptContext();
|
|
await evaluator.RunAsync(ctx, TestContext.Current.CancellationToken);
|
|
ctx.Writes.Count.ShouldBe(1);
|
|
ctx.Writes[0].Path.ShouldBe("Out");
|
|
ctx.Writes[0].Value.ShouldBe(42);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_File_IO_at_compile()
|
|
{
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, string>.Compile(
|
|
"""return System.IO.File.ReadAllText("c:/secrets.txt");"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_HttpClient_at_compile()
|
|
{
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var c = new System.Net.Http.HttpClient();
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Process_Start_at_compile()
|
|
{
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.Diagnostics.Process.Start("cmd.exe");
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Reflection_Assembly_Load_at_compile()
|
|
{
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.Reflection.Assembly.Load("System.Core");
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Environment_Exit_at_compile()
|
|
{
|
|
// System.Environment lives in System.Private.CoreLib (allow-listed for
|
|
// primitives) so a namespace-prefix deny-list cannot block it. Environment.Exit
|
|
// terminates the whole in-process OPC UA server — every connected client and
|
|
// every driver — so it MUST be rejected member-granularly. (Core.Scripting-001.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.Environment.Exit(0);
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Environment_FailFast_at_compile()
|
|
{
|
|
// Environment.FailFast crashes the host process immediately — same outage as
|
|
// Exit. (Core.Scripting-001.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.Environment.FailFast("boom");
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_AppDomain_at_compile()
|
|
{
|
|
// AppDomain.CurrentDomain exposes process-wide control (assembly load events,
|
|
// unhandled-exception hooks). Not script surface. (Core.Scripting-001.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var n = System.AppDomain.CurrentDomain.FriendlyName;
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_GC_Collect_at_compile()
|
|
{
|
|
// GC.Collect / GC.AddMemoryPressure let a script perturb the whole process's
|
|
// memory subsystem. Not script surface. (Core.Scripting-001.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.GC.Collect();
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Activator_CreateInstance_at_compile()
|
|
{
|
|
// Activator.CreateInstance is a reflection-equivalent escape — it can construct
|
|
// a forbidden type by name without ever naming it syntactically. (Core.Scripting-001.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var o = System.Activator.CreateInstance(typeof(object));
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Environment_GetEnvironmentVariable_at_compile()
|
|
{
|
|
// The whole System.Environment type is forbidden (Core.Scripting-001) — even the
|
|
// read-only GetEnvironmentVariable member. Once Exit / FailFast made the type
|
|
// dangerous, the cleanest member-granular rule is to deny the type outright; the
|
|
// read path has no legitimate use in a SCADA predicate either.
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, string?>.Compile(
|
|
"""return System.Environment.GetEnvironmentVariable("PATH");"""));
|
|
}
|
|
|
|
// --- Core.Scripting-002: type-reference node forms that bypassed the old walker ---
|
|
// The old walker only inspected ObjectCreation / Invocation-with-member-access /
|
|
// MemberAccess / bare-Identifier nodes. typeof, generic type arguments, casts,
|
|
// is/as patterns, default(T), array element types, and declared variable types all
|
|
// name a forbidden type without producing any of those nodes. The broadened walker
|
|
// resolves GetTypeInfo on every TypeSyntax / ExpressionSyntax so they are all caught.
|
|
|
|
[Fact]
|
|
public void Rejects_typeof_forbidden_type_at_compile()
|
|
{
|
|
// Phase 7 plan A.6 explicitly calls out typeof as a sandbox-escape that must
|
|
// fail at compile. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, string>.Compile(
|
|
"""return typeof(System.IO.File).Name;"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_generic_type_argument_forbidden_type_at_compile()
|
|
{
|
|
// new List<System.IO.FileInfo>() — the forbidden type is nested inside an
|
|
// allowed generic. The walker unwraps type arguments recursively. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var l = new System.Collections.Generic.List<System.IO.FileInfo>();
|
|
return l.Count;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_cast_to_forbidden_type_at_compile()
|
|
{
|
|
// (System.IO.Stream)null — a cast expression names the type without invoking
|
|
// or constructing it. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var s = (System.IO.Stream)null;
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_default_of_forbidden_type_at_compile()
|
|
{
|
|
// default(System.Reflection.Assembly) names a forbidden type via the default
|
|
// operator. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var a = default(System.Reflection.Assembly);
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_is_pattern_forbidden_type_at_compile()
|
|
{
|
|
// 'o is System.IO.FileStream' names a forbidden type as the pattern's type
|
|
// operand. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, bool>.Compile(
|
|
"""
|
|
object o = ctx.GetTag("X").Value;
|
|
return o is System.IO.FileStream;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_as_expression_forbidden_type_at_compile()
|
|
{
|
|
// 'o as System.IO.Stream' names a forbidden type via the as operator. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
object o = ctx.GetTag("X").Value;
|
|
var s = o as System.IO.Stream;
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_array_creation_forbidden_element_type_at_compile()
|
|
{
|
|
// new System.IO.FileInfo[0] — the forbidden type is the array element type.
|
|
// (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var a = new System.IO.FileInfo[0];
|
|
return a.Length;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_local_declared_variable_forbidden_type_at_compile()
|
|
{
|
|
// An explicitly-typed local declaration 'System.Net.Http.HttpClient c;' names
|
|
// a forbidden type with no construction or call. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.Net.Http.HttpClient c = null;
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_typeof_forbidden_type_inside_Activator_at_compile()
|
|
{
|
|
// Activator is already denied by ForbiddenFullTypeNames, but the typeof operand
|
|
// is independently a forbidden-type reference — confirm the typeof path catches
|
|
// it even when the Activator type itself were allowed. (Core.Scripting-002.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var t = typeof(System.Diagnostics.Process);
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Allowed_generic_type_argument_still_compiles()
|
|
{
|
|
// Guard against over-blocking: a generic with only allow-listed type arguments
|
|
// (List<int>) must still compile and run. (Core.Scripting-002.)
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var l = new System.Collections.Generic.List<int> { 1, 2, 3 };
|
|
return l.Count;
|
|
""");
|
|
var result = await evaluator.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken);
|
|
result.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Allowed_typeof_still_compiles()
|
|
{
|
|
// typeof against an allow-listed type (int) must not be rejected. (Core.Scripting-002.)
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, string>.Compile(
|
|
"""return typeof(int).Name;""");
|
|
var result = await evaluator.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken);
|
|
result.ShouldBe("Int32");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Script_exception_propagates_unwrapped()
|
|
{
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""throw new InvalidOperationException("boom");""");
|
|
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
|
await evaluator.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken));
|
|
}
|
|
|
|
[Fact]
|
|
public void Ctx_Now_is_available_without_DateTime_UtcNow_reaching_wall_clock()
|
|
{
|
|
// Scripts that need a timestamp go through ctx.Now so tests can pin it.
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, DateTime>.Compile("""return ctx.Now;""");
|
|
evaluator.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Deadband_helper_is_reachable_from_scripts()
|
|
{
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, bool>.Compile(
|
|
"""return ScriptContext.Deadband(10.5, 10.0, 0.3);""");
|
|
evaluator.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Linq_Enumerable_is_available_from_scripts()
|
|
{
|
|
// LINQ is in the allow-list because SCADA math frequently wants Sum / Average
|
|
// / Where. Confirm it works.
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var nums = new[] { 1, 2, 3, 4, 5 };
|
|
return nums.Where(n => n > 2).Sum();
|
|
""");
|
|
var result = await evaluator.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken);
|
|
result.ShouldBe(12);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DataValueSnapshot_is_usable_in_scripts()
|
|
{
|
|
// ctx.GetTag returns DataValueSnapshot so scripts branch on quality.
|
|
var evaluator = ScriptEvaluator<FakeScriptContext, bool>.Compile(
|
|
"""
|
|
var v = ctx.GetTag("T");
|
|
return v.StatusCode == 0;
|
|
""");
|
|
var ctx = new FakeScriptContext().Seed("T", 5.0);
|
|
var result = await evaluator.RunAsync(ctx, TestContext.Current.CancellationToken);
|
|
result.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Compile_error_gives_location_in_diagnostics()
|
|
{
|
|
// Compile errors must carry the source span so the Admin UI can point at them.
|
|
try
|
|
{
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile("""return fooBarBaz + 1;""");
|
|
Assert.Fail("expected CompilationErrorException");
|
|
}
|
|
catch (CompilationErrorException ex)
|
|
{
|
|
ex.Diagnostics.ShouldNotBeEmpty();
|
|
ex.Diagnostics[0].Location.ShouldNotBeNull();
|
|
}
|
|
}
|
|
|
|
// --- Core.Scripting-010: remaining forbidden-namespace vectors not previously tested ---
|
|
// System.Threading.Thread, System.Threading.Tasks, System.Runtime.InteropServices, and
|
|
// Microsoft.Win32 were all in ForbiddenNamespacePrefixes but had no test asserting their
|
|
// rejection. Adding them here closes the coverage gap that allowed Core.Scripting-001 and
|
|
// -002 to go undetected.
|
|
|
|
[Fact]
|
|
public void Rejects_Thread_new_at_compile()
|
|
{
|
|
// System.Threading.Thread is in ForbiddenNamespacePrefixes — raw thread creation
|
|
// in a script would bypass the per-evaluation timeout and tie up a thread-pool thread
|
|
// indefinitely. (Core.Scripting-010.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var t = new System.Threading.Thread(() => { });
|
|
t.Start();
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Tasks_TaskRun_at_compile()
|
|
{
|
|
// System.Threading.Tasks is now in ForbiddenNamespacePrefixes (Core.Scripting-003).
|
|
// Scripts are synchronous predicates — background tasks would outlive the evaluation
|
|
// timeout. (Core.Scripting-010.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var t = System.Threading.Tasks.Task.Run(() => 42);
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_InteropServices_at_compile()
|
|
{
|
|
// System.Runtime.InteropServices gives access to native memory and COM — clearly
|
|
// outside the safe predicate surface. (Core.Scripting-010.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var p = System.Runtime.InteropServices.Marshal.AllocHGlobal(256);
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Win32_Registry_at_compile()
|
|
{
|
|
// Microsoft.Win32 provides registry access — not appropriate from a sandboxed
|
|
// SCADA predicate. (Core.Scripting-010.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var k = Microsoft.Win32.Registry.CurrentUser;
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
// --- Core.Scripting-012: BCL refs broadened by -008/-016 re-exposed three vectors
|
|
// the original deny-list missed. Each is denied type-granularly in
|
|
// ForbiddenTypeAnalyzer.ForbiddenFullTypeNames; these tests pin the rejection.
|
|
|
|
[Fact]
|
|
public void Rejects_ThreadPool_QueueUserWorkItem_at_compile()
|
|
{
|
|
// System.Threading.ThreadPool.QueueUserWorkItem re-introduces the background-
|
|
// fanout threat Core.Scripting-003 closed against System.Threading.Tasks. The
|
|
// ThreadPool type lives in System.Threading (shared with allowed sync primitives
|
|
// like CancellationToken), so it must be denied type-granularly. (Core.Scripting-012.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
System.Threading.ThreadPool.QueueUserWorkItem(_ => { });
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_Timer_new_at_compile()
|
|
{
|
|
// System.Threading.Timer schedules unbounded callback work that outlives the
|
|
// per-evaluation timeout. Same namespace as CancellationToken so type-granular.
|
|
// (Core.Scripting-012.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var t = new System.Threading.Timer(_ => { });
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_AssemblyLoadContext_at_compile()
|
|
{
|
|
// System.Runtime.Loader.AssemblyLoadContext loads arbitrary DLLs into the
|
|
// process — defense-in-depth gap that the BCL-wide reference set re-opened.
|
|
// System.Reflection already denies the typical invocation path, but the
|
|
// sandbox boundary stays type-explicit. (Core.Scripting-012.)
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var alc = System.Runtime.Loader.AssemblyLoadContext.Default;
|
|
return 0;
|
|
"""));
|
|
}
|
|
|
|
// --- Core.Scripting-013: wrapper-source injection ---
|
|
|
|
[Fact]
|
|
public void Rejects_sibling_method_injection_via_balanced_braces()
|
|
{
|
|
// Core.Scripting-013 — a brace-balanced source that closes Run early and
|
|
// declares a sibling method inside CompiledScript. The analyzer would still
|
|
// walk the injected member, but the documented "method body" contract
|
|
// forbids the shape outright now.
|
|
var ex = Should.Throw<CompilationErrorException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
return 0;
|
|
} public static int Evil() { return 1;
|
|
"""));
|
|
ex.Message.ShouldContain("Core.Scripting-013");
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejects_sibling_class_injection_via_balanced_braces()
|
|
{
|
|
// Same shape, but injecting an entire sibling class inside the synthesized
|
|
// namespace. Reject at the type-count check.
|
|
var ex = Should.Throw<CompilationErrorException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
return 0;
|
|
}
|
|
}
|
|
public static class CompiledScript2 { public static int M() { return 0; } }
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Compiled { public static class CompiledScript3 { public static int M() {
|
|
return 0;
|
|
"""));
|
|
ex.Message.ShouldContain("Core.Scripting-013");
|
|
}
|
|
}
|