diff --git a/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs b/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs
index 8bc6334..70fe0fc 100644
--- a/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs
+++ b/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs
@@ -221,7 +221,7 @@ public class QueryComparator
// Normalize whitespace and case
return System.Text.RegularExpressions.Regex
- .Replace(clause.Trim(), @"\s+", " ")
+ .Replace(clause.Trim(), @"\s+", " ", System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)
.ToUpperInvariant();
}
diff --git a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs
index 4429de3..54f187c 100644
--- a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs
+++ b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using Strata.SqlTools.SqlBreakdown.Expressions;
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
using CommandVisitor = Strata.SqlTools.Visitors.PostgreSql.CommandVisitor;
@@ -88,6 +89,14 @@ public class QueryBreakdown : SqlServerQueryBreakdown
}
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
+
///
/// Adds a parameter to the query using PostgreSQL's positional parameter format ($1, $2, ...).
///
diff --git a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs
index 36d71ef..5a1b308 100644
--- a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs
+++ b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs
@@ -1,3 +1,4 @@
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Interfaces;
@@ -35,6 +36,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
_queryBreakdowns = queryBreakdowns?.ToList() ?? new List();
}
+ ///
+ /// Validates that the backing list survived deserialization, since deserialization bypasses
+ /// the constructors that normally initialize it (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ if (_queryBreakdowns is null)
+ {
+ throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
+ }
+ }
+
///
/// Gets the collection of QueryBreakdown objects.
///
diff --git a/src/Strata.SqlTools.Query/CalculationFilterGroup.cs b/src/Strata.SqlTools.Query/CalculationFilterGroup.cs
index 29653ee..84e0fce 100644
--- a/src/Strata.SqlTools.Query/CalculationFilterGroup.cs
+++ b/src/Strata.SqlTools.Query/CalculationFilterGroup.cs
@@ -2,18 +2,24 @@ using System.Text.Json.Serialization;
namespace Strata.SqlTools.Query;
-[method: JsonConstructor]
-public class CalculationFilterGroup(IEnumerable filters, LogicalOperator logicalOperator)
+public class CalculationFilterGroup
{
- // Hereditary logical operation applied to all Filters
- public LogicalOperator LogicalOperator { get; set; } = logicalOperator;
-
- public IEnumerable Filters { get; set; } = filters;
+ [JsonConstructor]
+ public CalculationFilterGroup(IEnumerable filters, LogicalOperator logicalOperator)
+ {
+ Filters = filters;
+ LogicalOperator = logicalOperator;
+ }
public CalculationFilterGroup() : this([], LogicalOperator.And)
{
}
+ // Hereditary logical operation applied to all Filters
+ public LogicalOperator LogicalOperator { get; set; }
+
+ public IEnumerable Filters { get; set; }
+
public IEnumerable GetValidFilters()
{
return Filters?.Where(x => x.IsValid()).ToList() ?? [];
diff --git a/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs b/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs
index d5e7c32..ca1c590 100644
--- a/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs
+++ b/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs
@@ -8,6 +8,12 @@ namespace Strata.SqlTools.Rules.Rule.Expression;
///
public static class Markdown
{
+ ///
+ /// Match timeout applied to all regular expressions to guard against catastrophic
+ /// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444).
+ ///
+ private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
+
private static readonly Dictionary> LogicalOperators = new()
{
{ "\\land", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
@@ -70,12 +76,12 @@ public static class Markdown
private static string StripMarkdownDelimiters(string text)
{
// Remove $...$ or $$...$$ delimiters
- text = Regex.Replace(text, @"^\$\$?\s*", "");
- text = Regex.Replace(text, @"\s*\$\$?$", "");
+ text = Regex.Replace(text, @"^\$\$?\s*", "", RegexOptions.None, RegexTimeout);
+ text = Regex.Replace(text, @"\s*\$\$?$", "", RegexOptions.None, RegexTimeout);
// Remove ```math...``` code fence
- text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline);
- text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline);
+ text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline, RegexTimeout);
+ text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline, RegexTimeout);
return text.Trim();
}
@@ -186,18 +192,18 @@ public static class Markdown
private static Expression? TryParseProperty(string text)
{
// Parse property access (e.g., x.PropertyName or \text{x.PropertyName})
- var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
+ var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout);
if (propertyMatch.Success)
{
return new Property(propertyMatch.Groups[1].Value, propertyMatch.Groups[2].Value);
}
// Parse \text{...} property access
- var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$");
+ var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$", RegexOptions.None, RegexTimeout);
if (textMatch.Success)
{
var textContent = textMatch.Groups[1].Value;
- var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
+ var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout);
if (propMatch.Success)
{
return new Property(propMatch.Groups[1].Value, propMatch.Groups[2].Value);
@@ -215,7 +221,7 @@ public static class Markdown
}
// Single property name
- if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
+ if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout))
{
return new Property(textContent);
}
@@ -236,7 +242,7 @@ public static class Markdown
}
// Parse simple property without parameter
- if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
+ if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout))
{
return new Property(text);
}
@@ -247,7 +253,7 @@ public static class Markdown
private static Expression? TryParseLiteral(string text)
{
// Parse string literals (quoted)
- var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$");
+ var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$", RegexOptions.None, RegexTimeout);
if (stringMatch.Success)
{
return new StringLiteral(stringMatch.Groups[1].Value);
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs
index d0708db..3bfb2b6 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using System.Text;
using SqlServerDeleteBreakdown = Strata.SqlTools.Breakdowns.SqlServer.DeleteBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
@@ -40,6 +41,14 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
+
///
/// Gets the SQL breakdown as a string for Snowflake.
///
@@ -145,7 +154,7 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
// Check if it's a DELETE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase))
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with DELETE.";
return false;
@@ -161,7 +170,7 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown
// Snowflake uses simpler DELETE syntax: DELETE FROM table WHERE condition
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!deleteMatch.Success)
{
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs
index 3e89ef8..eff1562 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using Strata.SqlTools.SqlBreakdown.Utilities;
using SqlServerInsertBreakdown = Strata.SqlTools.Breakdowns.SqlServer.InsertBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
@@ -65,6 +66,14 @@ public class InsertBreakdown : SqlServerInsertBreakdown
ValuesClause.Clause = string.Join(",", valuesList);
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
+
#region Parse Methods
///
@@ -149,7 +158,7 @@ public class InsertBreakdown : SqlServerInsertBreakdown
// Check if it's an INSERT statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase))
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with INSERT INTO.";
return false;
@@ -165,7 +174,7 @@ public class InsertBreakdown : SqlServerInsertBreakdown
// Parse INSERT statement using regex
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!insertMatch.Success)
{
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs
index 99056a0..cb2c65b 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using System.Text;
using SqlServerProcedureBreakdown = Strata.SqlTools.Breakdowns.SqlServer.ProcedureBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
@@ -46,6 +47,15 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
Parameters = parameters ?? new Dictionary();
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the procedure name and parameter collection
+ /// (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
+
///
/// Gets the SQL breakdown as a string for Snowflake.
///
@@ -157,7 +167,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
// Check if it's a CALL statement (Snowflake syntax) or EXEC (for compatibility)
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*(CALL|EXEC|EXECUTE)\b",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase))
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE.";
return false;
@@ -174,7 +184,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
// Pattern: CALL procedureName(param => value, ...)
var callMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"(?:CALL|EXEC|EXECUTE)\s+([^\s\(]+)(?:\s*\((.*?)\))?$",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!callMatch.Success)
{
@@ -192,7 +202,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown
// Parse parameters - Snowflake uses param => value syntax
var paramMatches = System.Text.RegularExpressions.Regex.Matches(parametersText,
@"(\w+)\s*=>\s*([^,]+)(?:,|$)",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
foreach (System.Text.RegularExpressions.Match paramMatch in paramMatches)
{
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
index bc9fe97..de946fd 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Expressions;
@@ -85,6 +86,14 @@ public class QueryBreakdown : SqlServerQueryBreakdown
OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : null;
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
+
///
/// Adds a parameter to the query using Snowflake's :param format.
/// Also adds @param format for compatibility.
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs
index 3e18e92..062f0eb 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs
@@ -1,3 +1,4 @@
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Interfaces;
@@ -36,6 +37,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
_queryBreakdowns = new List(queryBreakdowns ?? Enumerable.Empty());
}
+ ///
+ /// Validates that the backing list survived deserialization, since deserialization bypasses
+ /// the constructors that normally initialize it (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ if (_queryBreakdowns is null)
+ {
+ throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
+ }
+ }
+
///
/// Gets the collection of QueryBreakdown objects.
///
@@ -170,7 +185,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
// This avoids false positives from @parameter syntax
var stagePattern = @"@[\w~]+/";
- if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern))
+ if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern, System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
return false;
}
@@ -184,7 +199,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/"
: $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/";
- return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
});
}
diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs
index a02b190..21e5341 100644
--- a/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs
+++ b/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using System.Text;
using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown;
using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser;
@@ -45,6 +46,14 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null;
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
+
///
/// Gets the SQL breakdown as a string for Snowflake.
///
@@ -160,7 +169,7 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
// Check if it's an UPDATE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase))
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with UPDATE.";
return false;
@@ -176,7 +185,7 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown
// Parse UPDATE statement - handle both with and without FROM clause
var updateMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!updateMatch.Success)
{
diff --git a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs
index a04cc67..18252f9 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs
@@ -48,6 +48,18 @@ public abstract class SqlBreakdownBase : ISqlBreakdown
///
public bool IsUsingFinishClause => FinishClauses.Count > 0;
+ ///
+ /// Re-establishes the invariants normally guaranteed by the constructors after the object
+ /// is reconstructed by deserialization. Deserialization bypasses constructors, so the
+ /// collection state must be re-validated to avoid a partially-initialized object
+ /// (SonarQube rule S5766).
+ ///
+ protected void RevalidateBreakdownState()
+ {
+ SetupClauses ??= new List();
+ FinishClauses ??= new ArrayList();
+ }
+
///
/// Gets the SQL breakdown as a string. Must be implemented by derived classes.
///
diff --git a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs
index c5f47bc..9ecd6cf 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs
@@ -1,3 +1,4 @@
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Interfaces;
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
@@ -35,6 +36,20 @@ public class SqlBreakdownCollection : ICollection
_breakdowns = new List(breakdowns ?? Enumerable.Empty());
}
+ ///
+ /// Validates that the backing list survived deserialization, since deserialization bypasses
+ /// the constructors that normally initialize it (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ if (_breakdowns is null)
+ {
+ throw new SerializationException("Deserialized SqlBreakdownCollection is missing its backing list.");
+ }
+ }
+
///
/// Gets the collection of SQL breakdowns.
///
diff --git a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs
index 2c1b2da..17bea2b 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs
@@ -1,3 +1,4 @@
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
@@ -58,6 +59,21 @@ public class SqlFilter : ISqlAppendable
}
}
+ ///
+ /// Validates that the expression and parameter state survived deserialization, since
+ /// deserialization bypasses the constructors that normally initialize them and enforce the
+ /// even parameter-name/value pairing (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ if (_sqlExpression is null || _parameterValues is null)
+ {
+ throw new SerializationException("Deserialized SqlFilter is missing its expression or parameter state.");
+ }
+ }
+
///
/// Gets or sets the SQL expression.
///
diff --git a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs
index 0eb8678..a8fd6f6 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs
@@ -1,5 +1,6 @@
using System.Text;
using System.Text.RegularExpressions;
+using Strata.SqlTools.SqlBreakdown.Utilities;
namespace Strata.SqlTools.SqlBreakdown.Extensions;
@@ -10,7 +11,7 @@ public class StringBuilderEx
{
private static readonly Regex AppendFormatExRegex = new Regex(
@"\{(?.*?)(?!.*?)?\}",
- RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
private readonly StringBuilder _innerStringBuilder;
diff --git a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs
index 90550e9..602273a 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
+using Strata.SqlTools.SqlBreakdown.Utilities;
namespace Strata.SqlTools.SqlBreakdown.Extensions;
@@ -119,7 +120,7 @@ public static class StringExtensions
public static bool IsGUID(this string aString)
{
const string pattern = "^[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}$";
- var match = Regex.Match(aString, pattern);
+ var match = Regex.Match(aString, pattern, RegexOptions.None, RegexDefaults.MatchTimeout);
return match.Success;
}
diff --git a/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs b/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs
index d847f43..a869cd0 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs
@@ -226,7 +226,7 @@ public static class GuidUtils
private static readonly Regex FindFirstGuidRegex = new Regex(
"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexDefaults.MatchTimeout);
#endregion
@@ -292,7 +292,7 @@ public static class GuidUtils
/// A list of GUID strings found in the input.
public static List GetGuids(string value)
{
- MatchCollection matches = Regex.Matches(value, GUID_STRING);
+ MatchCollection matches = Regex.Matches(value, GUID_STRING, RegexOptions.None, RegexDefaults.MatchTimeout);
return matches.Cast().Select(x => x.Value).ToList();
}
diff --git a/src/Strata.SqlTools.SqlBreakdown/Utilities/RegexDefaults.cs b/src/Strata.SqlTools.SqlBreakdown/Utilities/RegexDefaults.cs
new file mode 100644
index 0000000..879feb5
--- /dev/null
+++ b/src/Strata.SqlTools.SqlBreakdown/Utilities/RegexDefaults.cs
@@ -0,0 +1,14 @@
+namespace Strata.SqlTools.SqlBreakdown.Utilities;
+
+///
+/// Shared defaults for usage across the SQL tools.
+///
+public static class RegexDefaults
+{
+ ///
+ /// Default match timeout applied to regular expressions to guard against catastrophic
+ /// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444). Regular expressions
+ /// in this library process arbitrary SQL text, so every pattern is given a bounded execution time.
+ ///
+ public static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(1);
+}
diff --git a/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs b/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs
index 8264275..ec6d6ee 100644
--- a/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs
+++ b/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs
@@ -115,7 +115,7 @@ public static partial class SqlUtils
public static string GetSqlFriendlyName(string str, string replacement = "")
{
// Invalid characters --> . , ; ' ` : / \ * | ? " & % $ ! + = ( ) [ ] { } - ~ ^
- return new Regex(@"[\.,;'`:/\\*\|?""&%\$!\+=\(\)\[\]{}\-~\ \^]").Replace(str, replacement);
+ return new Regex(@"[\.,;'`:/\\*\|?""&%\$!\+=\(\)\[\]{}\-~\ \^]", RegexOptions.None, RegexDefaults.MatchTimeout).Replace(str, replacement);
}
#endregion
@@ -382,37 +382,37 @@ public static partial class SqlUtils
{
string msg = ex.Message;
- Match match = Regex.Match(msg, @".*?Violation of UNIQUE KEY constraint (.*?) Cannot insert duplicate key in object (.*?)");
+ Match match = Regex.Match(msg, @".*?Violation of UNIQUE KEY constraint (.*?) Cannot insert duplicate key in object (.*?)", RegexOptions.None, RegexDefaults.MatchTimeout);
if (match.Success)
{
return WellKnownSqlError.UniqueKeyViolation;
}
- match = Regex.Match(msg, @".*Cannot insert duplicate key row in object.*", RegexOptions.IgnoreCase);
+ match = Regex.Match(msg, @".*Cannot insert duplicate key row in object.*", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (match.Success)
{
return WellKnownSqlError.UniqueKeyViolation;
}
- match = Regex.Match(msg, @".*Update or insert of view or function (.*?) failed because it contains a derived or constant field.*");
+ match = Regex.Match(msg, @".*Update or insert of view or function (.*?) failed because it contains a derived or constant field.*", RegexOptions.None, RegexDefaults.MatchTimeout);
if (match.Success)
{
return WellKnownSqlError.UpdateViewMultipleTables;
}
- match = Regex.Match(msg, @".*There is already an object named (.*?) in the database.*");
+ match = Regex.Match(msg, @".*There is already an object named (.*?) in the database.*", RegexOptions.None, RegexDefaults.MatchTimeout);
if (match.Success)
{
return WellKnownSqlError.ObjectAlreadyExists;
}
- match = Regex.Match(msg, @".*Cannot truncate table (.*?) because it is being referenced by a FOREIGN KEY constraint.*");
+ match = Regex.Match(msg, @".*Cannot truncate table (.*?) because it is being referenced by a FOREIGN KEY constraint.*", RegexOptions.None, RegexDefaults.MatchTimeout);
if (match.Success)
{
return WellKnownSqlError.TruncateTableForeignKeyReferenceError;
}
- match = Regex.Match(msg, @".*Could not truncate object (.*?) because it is not a table.*");
+ match = Regex.Match(msg, @".*Could not truncate object (.*?) because it is not a table.*", RegexOptions.None, RegexDefaults.MatchTimeout);
if (match.Success)
{
return WellKnownSqlError.TruncateTableNotATableError;
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs
index 96e77db..fd8c44b 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs
@@ -145,7 +145,7 @@ public class DeleteBreakdown : SqlBreakdownBase
// Check if it's a DELETE statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase))
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with DELETE.";
return false;
@@ -162,14 +162,14 @@ public class DeleteBreakdown : SqlBreakdownBase
// Pattern: DELETE [table_alias] FROM table WHERE condition
var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"DELETE\s+(.*?)\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!deleteMatch.Success)
{
// Try simpler pattern: DELETE FROM table WHERE condition
deleteMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!deleteMatch.Success)
{
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs
index e8268b6..071e7d0 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs
@@ -158,7 +158,7 @@ public class InsertBreakdown : SqlBreakdownBase
// Check if it's an INSERT statement
var sqlTrimmed = sql.TrimStart();
if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase))
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with INSERT INTO.";
return false;
@@ -175,7 +175,7 @@ public class InsertBreakdown : SqlBreakdownBase
// Pattern: INSERT INTO table (columns) VALUES (values)
var insertMatch = System.Text.RegularExpressions.Regex.Match(sql,
@"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!insertMatch.Success)
{
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs
index 84bb93b..cf697a4 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using Strata.SqlTools.SqlBreakdown.Classes;
@@ -45,6 +46,20 @@ public class ProcedureBreakdown : SqlBreakdownBase
Parameters = parameters ?? new Dictionary();
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the procedure name and parameter collection
+ /// (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ RevalidateBreakdownState();
+ ProcedureName ??= new SqlClause();
+ Parameters ??= new Dictionary();
+ }
+
///
/// Gets or sets the stored procedure name.
///
@@ -169,7 +184,7 @@ public class ProcedureBreakdown : SqlBreakdownBase
// Check if it's an EXEC or EXECUTE statement
var sqlTrimmed = sql.TrimStart();
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(EXEC|EXECUTE)\b",
- RegexOptions.IgnoreCase))
+ RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with EXEC or EXECUTE.";
return false;
@@ -186,7 +201,7 @@ public class ProcedureBreakdown : SqlBreakdownBase
// Pattern: EXEC[UTE] procedureName [@param = value, ...]
var execMatch = Regex.Match(sql,
@"(?:EXEC|EXECUTE)\s+([^\s@,]+)(?:\s+(.*))?$",
- RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!execMatch.Success)
{
@@ -204,7 +219,7 @@ public class ProcedureBreakdown : SqlBreakdownBase
// Parse parameters - handle both @param = value and positional parameters
var paramMatches = Regex.Matches(parametersText,
@"(@\w+)\s*=\s*([^,]+)(?:,|$)",
- RegexOptions.IgnoreCase);
+ RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
parameters = paramMatches
.Cast()
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs
index 2395103..e817b91 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Expressions;
@@ -107,6 +108,28 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
}
}
+ ///
+ /// Re-establishes invariants after deserialization, since deserialization bypasses the
+ /// constructors that normally initialize the parameter, WITH-clause, and clause backing
+ /// fields (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ RevalidateBreakdownState();
+
+ _parameterList ??= new List();
+ _withClauses ??= new List();
+ _selectClause ??= new SqlExpressionClause(splitOnComma: true);
+ _fromClause ??= new SqlClause();
+ _whereClause ??= new SqlExpressionClause(splitOnComma: false);
+ _groupByClause ??= new SqlExpressionClause(splitOnComma: true);
+ _havingClause ??= new SqlExpressionClause(splitOnComma: false);
+ _orderByClause ??= new SqlExpressionClause(splitOnComma: true);
+ _clausesCacheDirty = true;
+ }
+
#region Properties
///
@@ -1308,7 +1331,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown
{
// Try to extract position from error message
var match = System.Text.RegularExpressions.Regex.Match(error, @"position[:\s]+(\d+)",
- System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPos))
{
position = parsedPos;
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs
index 3cebcb5..0594a9b 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs
@@ -1,3 +1,4 @@
+using System.Runtime.Serialization;
using System.Text;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Interfaces;
@@ -33,6 +34,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection
_queryBreakdowns = new List(queryBreakdowns ?? Enumerable.Empty());
}
+ ///
+ /// Validates that the backing list survived deserialization, since deserialization bypasses
+ /// the constructors that normally initialize it (SonarQube rule S5766).
+ ///
+ /// The streaming context for the deserialization operation.
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ if (_queryBreakdowns is null)
+ {
+ throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
+ }
+ }
+
///
/// Gets the collection of QueryBreakdown objects.
///
diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs
index 3acfe1f..f5ee6b6 100644
--- a/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs
+++ b/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs
@@ -165,7 +165,7 @@ public class UpdateBreakdown : SqlBreakdownBase
// Check if it's an UPDATE statement
var sqlTrimmed = sql.TrimStart();
if (!Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b",
- RegexOptions.IgnoreCase))
+ RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with UPDATE.";
return false;
@@ -182,7 +182,7 @@ public class UpdateBreakdown : SqlBreakdownBase
// Pattern: UPDATE table SET column=value [FROM table] [WHERE condition]
var updateMatch = Regex.Match(sql,
@"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$",
- RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout);
if (!updateMatch.Success)
{
diff --git a/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs b/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs
index 3795ede..c2b7d5c 100644
--- a/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs
+++ b/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs
@@ -3,6 +3,7 @@ using System.Text;
using System.Text.RegularExpressions;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
+using Strata.SqlTools.SqlBreakdown.Utilities;
namespace Strata.SqlTools.Statements.SqlServer;
@@ -35,7 +36,7 @@ public class StatementParser
// Remove SQL comments before processing
sql = RemoveSqlComments(sql);
// Replace multiple whitespace/newlines with single space
- sql = Regex.Replace(sql, @"\s+", " ");
+ sql = Regex.Replace(sql, @"\s+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
return sql.Trim();
}
@@ -47,7 +48,7 @@ public class StatementParser
public virtual string NormalizeSqlPreservingComments(string sql)
{
// Replace multiple spaces/tabs with single space, but preserve newlines for comment handling
- sql = Regex.Replace(sql, @"[ \t]+", " ");
+ sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
// Remove leading/trailing whitespace from each line
var lines = sql.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
sql = string.Join("\n", lines.Select(line => line.Trim()));
@@ -238,7 +239,7 @@ public class StatementParser
// Simple extraction: look for statements before the main SELECT
var selectIndex = Regex.Match(
- sql, @"\bSELECT\b", RegexOptions.IgnoreCase).Index;
+ sql, @"\bSELECT\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout).Index;
if (selectIndex > 0)
{
@@ -275,7 +276,7 @@ public class StatementParser
// Look for DROP TABLE or other cleanup statements after the main query
var finishPattern = GetFinishClausePattern();
var match = Regex.Match(
- sql, finishPattern, RegexOptions.IgnoreCase);
+ sql, finishPattern, RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (match.Success)
{
@@ -302,7 +303,7 @@ public class StatementParser
// Check if SQL starts with WITH
var withMatch = Regex.Match(
- sql, @"^\s*WITH\b", RegexOptions.IgnoreCase);
+ sql, @"^\s*WITH\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (!withMatch.Success)
{
@@ -330,7 +331,7 @@ public class StatementParser
// Check if we're at a SELECT keyword at top level
var selectMatch = Regex.Match(
sql.Substring(i), @"^\s*SELECT\b",
- RegexOptions.IgnoreCase);
+ RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (selectMatch.Success && IsTopLevelKeyword(sql, i + selectMatch.Index))
{
@@ -398,7 +399,7 @@ public class StatementParser
}
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(WITH|SELECT)\b",
- RegexOptions.IgnoreCase))
+ RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with WITH or SELECT.";
clauses = null;
@@ -830,7 +831,7 @@ public class StatementParser
/// The regex pattern to match parameter names.
protected virtual void ExtractParameters(Dictionary parameters, string sql, string paramPattern)
{
- var matches = Regex.Matches(sql, paramPattern);
+ var matches = Regex.Matches(sql, paramPattern, RegexOptions.None, RegexDefaults.MatchTimeout);
var paramNames = matches.Cast()
.Select(match => match.Value)