diff --git a/OAT.Scripting/ScriptOperation.cs b/OAT.Scripting/ScriptOperation.cs index 8d3543e5..53af775e 100644 --- a/OAT.Scripting/ScriptOperation.cs +++ b/OAT.Scripting/ScriptOperation.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; @@ -30,6 +31,12 @@ public ScriptOperation(Analyzer analyzer) : base(Operation.Script, analyzer) internal IEnumerable ScriptOperationValidationDelegate(Rule rule, Clause clause) { + if (Analyzer?.Options.RunScripts != true) + { + yield return new Violation(string.Format(Strings.Get("Err_ScriptingDisabled_{0}{1}"), rule.Name, clause.Label ?? rule.Clauses.IndexOf(clause).ToString(CultureInfo.InvariantCulture)), rule, clause); + yield break; + } + if (clause.Script is ScriptData clauseScript) { var issues = new List(); @@ -40,7 +47,21 @@ internal IEnumerable ScriptOperationValidationDelegate(Rule rule, Cla options = options.AddImports(clauseScript.Imports); options = options.AddReferences(typeof(Analyzer).Assembly); - options = options.AddReferences(clauseScript.References.Select(Assembly.Load)); + // Resolve assembly references without Assembly.Load to avoid triggering + // module initializers during validation (which should be side-effect free). + foreach (var reference in clauseScript.References) + { + var resolvedPath = ResolveAssemblyPath(reference); + if (resolvedPath != null) + { + options = options.AddReferences(MetadataReference.CreateFromFile(resolvedPath)); + } + else + { + issues.Add(new Violation(string.Format(Strings.Get("Err_ClauseInvalidLambda_{0}{1}{2}"), rule.Name, clause.Label ?? rule.Clauses.IndexOf(clause).ToString(CultureInfo.InvariantCulture), $"Could not resolve assembly reference '{reference}'"), rule, clause)); + } + } + var script = CSharpScript.Create(clauseScript.Code, globalsType: typeof(OperationArguments), options: options); foreach (var issue in script.Compile()) @@ -67,10 +88,47 @@ internal IEnumerable ScriptOperationValidationDelegate(Rule rule, Cla } } + /// + /// Resolves an assembly name to a file path without loading it into the runtime. + /// This avoids triggering module initializers that Assembly.Load would execute. + /// + private static string? ResolveAssemblyPath(string assemblyName) + { + // Check already-loaded assemblies (no new loading occurs) + var loaded = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); + if (loaded != null && !string.IsNullOrEmpty(loaded.Location)) + { + return loaded.Location; + } + + // Try the application base directory + var basePath = Path.Combine(AppContext.BaseDirectory, assemblyName + ".dll"); + if (File.Exists(basePath)) + { + return basePath; + } + + // Try the runtime directory + var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + var runtimePath = Path.Combine(runtimeDir, assemblyName + ".dll"); + if (File.Exists(runtimePath)) + { + return runtimePath; + } + + return null; + } + private Dictionary?> lambdas { get; } = new Dictionary?>(); internal OperationResult ScriptOperationDelegate(Clause clause, object? state1, object? state2, IEnumerable? captures) { + if (Analyzer?.Options.RunScripts != true) + { + return new OperationResult(false, null); + } + if (clause.Script is ScriptData scriptData) { if (!lambdas.ContainsKey(clause.Script)) diff --git a/OAT/Operations/RegexOperation.cs b/OAT/Operations/RegexOperation.cs index 14ed2dc6..d2b410c3 100644 --- a/OAT/Operations/RegexOperation.cs +++ b/OAT/Operations/RegexOperation.cs @@ -71,40 +71,47 @@ internal OperationResult RegexOperationDelegate(Clause clause, object? state1, o if (regex != null) { - foreach (var state in stateOneList) + try { - var matches = regex.Matches(state); - - if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) + foreach (var state in stateOneList) { - var outmatches = new List(); - foreach (var match in matches) + var matches = regex.Matches(state); + + if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) { - if (match is Match m) + var outmatches = new List(); + foreach (var match in matches) { - outmatches.Add(m); + if (match is Match m) + { + outmatches.Add(m); + } } + return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state1)); } - return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state1)); } - } - foreach (var state in stateTwoList) - { - var matches = regex.Matches(state); - - if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) + foreach (var state in stateTwoList) { - var outmatches = new List(); - foreach (var match in matches) + var matches = regex.Matches(state); + + if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) { - if (match is Match m) + var outmatches = new List(); + foreach (var match in matches) { - outmatches.Add(m); + if (match is Match m) + { + outmatches.Add(m); + } } + return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state2: state2)); } - return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state2: state2)); } } + catch (RegexMatchTimeoutException) + { + Log.Warning("Regex match timed out for pattern {0}. Treating as non-match.", built); + } } } return new OperationResult(false, null); @@ -122,7 +129,7 @@ internal OperationResult RegexOperationDelegate(Clause clause, object? state1, o { try { - RegexCache.TryAdd((built, regexOptions), new Regex(built, regexOptions)); + RegexCache.TryAdd((built, regexOptions), new Regex(built, regexOptions, TimeSpan.FromSeconds(5))); } catch (ArgumentException) {