From 3b107c2d01be911d149ae8fa8ea296ead72817a6 Mon Sep 17 00:00:00 2001
From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com>
Date: Wed, 14 Dec 2022 17:06:57 +0300
Subject: [PATCH] implement wildcard lenght quantifiers, TreatAsRegex property
and solve catastrpohic backtracking (#2528)
---
.../Commands/ComponentInteractionAttribute.cs | 6 ++++
.../Commands/ModalInteractionAttribute.cs | 5 +++
.../Builders/Commands/CommandBuilder.cs | 20 +++++++++++
.../Builders/Commands/ICommandBuilder.cs | 14 ++++++++
.../Builders/ModuleClassBuilder.cs | 4 ++-
.../Info/Commands/CommandInfo.cs | 3 ++
.../Info/ICommandInfo.cs | 2 ++
.../Map/CommandMapNode.cs | 9 ++---
.../Utilities/RegexUtils.cs | 33 +++++++++++++++++++
9 files changed, 89 insertions(+), 7 deletions(-)
diff --git a/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs
index 70bc285fc..823410cdf 100644
--- a/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.CompilerServices;
namespace Discord.Interactions
{
@@ -28,6 +29,11 @@ namespace Discord.Interactions
///
public RunMode RunMode { get; }
+ ///
+ /// Gets or sets whether the should be treated as a raw Regex pattern.
+ ///
+ public bool TreatAsRegex { get; set; } = false;
+
///
/// Create a command for component interaction handling.
///
diff --git a/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs
index a0ce91cda..f5df950bd 100644
--- a/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs
@@ -28,6 +28,11 @@ namespace Discord.Interactions
///
public RunMode RunMode { get; }
+ ///
+ /// Gets or sets whether the should be treated as a raw Regex pattern.
+ ///
+ public bool TreatAsRegex { get; set; } = false;
+
///
/// Create a command for modal interaction handling.
///
diff --git a/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs
index 5c35e8871..d7f90678d 100644
--- a/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs
@@ -35,6 +35,9 @@ namespace Discord.Interactions.Builders
///
public bool IgnoreGroupNames { get; set; }
+ ///
+ public bool TreatNameAsRegex { get; set; }
+
///
public RunMode RunMode { get; set; }
@@ -117,6 +120,19 @@ namespace Discord.Interactions.Builders
return Instance;
}
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public TBuilder WithNameAsRegex (bool value)
+ {
+ TreatNameAsRegex = value;
+ return Instance;
+ }
+
///
/// Adds parameter builders to .
///
@@ -163,6 +179,10 @@ namespace Discord.Interactions.Builders
ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) =>
SetRunMode(runMode);
+ ///
+ ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) =>
+ WithNameAsRegex(value);
+
///
ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) =>
AddParameters(parameters as TParamBuilder);
diff --git a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs
index 95007296c..97bc1e8e9 100644
--- a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs
@@ -34,6 +34,11 @@ namespace Discord.Interactions.Builders
///
bool IgnoreGroupNames { get; set; }
+ ///
+ /// Gets or sets whether the should be directly used as a Regex pattern.
+ ///
+ bool TreatNameAsRegex { get; set; }
+
///
/// Gets or sets the run mode this command gets executed with.
///
@@ -90,6 +95,15 @@ namespace Discord.Interactions.Builders
///
ICommandBuilder SetRunMode (RunMode runMode);
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ ICommandBuilder WithNameAsRegex(bool value);
+
///
/// Adds parameter builders to .
///
diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
index 35126a674..82acd800d 100644
--- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
@@ -274,6 +274,7 @@ namespace Discord.Interactions.Builders
builder.Name = interaction.CustomId;
builder.RunMode = interaction.RunMode;
builder.IgnoreGroupNames = interaction.IgnoreGroupNames;
+ builder.TreatNameAsRegex = interaction.TreatAsRegex;
}
break;
case PreconditionAttribute precondition:
@@ -287,7 +288,7 @@ namespace Discord.Interactions.Builders
var parameters = methodInfo.GetParameters();
- var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count;
+ var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp);
foreach (var parameter in parameters)
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount));
@@ -355,6 +356,7 @@ namespace Discord.Interactions.Builders
builder.Name = modal.CustomId;
builder.RunMode = modal.RunMode;
builder.IgnoreGroupNames = modal.IgnoreGroupNames;
+ builder.TreatNameAsRegex = modal.TreatAsRegex;
}
break;
case PreconditionAttribute precondition:
diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
index 99895d3ed..92e2f30bb 100644
--- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
@@ -66,6 +66,8 @@ namespace Discord.Interactions
///
public abstract IReadOnlyList Parameters { get; }
+ public bool TreatNameAsRegex { get; }
+
internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService)
{
CommandService = commandService;
@@ -78,6 +80,7 @@ namespace Discord.Interactions
RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode;
Attributes = builder.Attributes.ToImmutableArray();
Preconditions = builder.Preconditions.ToImmutableArray();
+ TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards;
_action = builder.Callback;
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
diff --git a/src/Discord.Net.Interactions/Info/ICommandInfo.cs b/src/Discord.Net.Interactions/Info/ICommandInfo.cs
index 843d5198b..1de6e0df7 100644
--- a/src/Discord.Net.Interactions/Info/ICommandInfo.cs
+++ b/src/Discord.Net.Interactions/Info/ICommandInfo.cs
@@ -65,6 +65,8 @@ namespace Discord.Interactions
///
IReadOnlyCollection Parameters { get; }
+ bool TreatNameAsRegex { get; }
+
///
/// Executes the command with the provided context.
///
diff --git a/src/Discord.Net.Interactions/Map/CommandMapNode.cs b/src/Discord.Net.Interactions/Map/CommandMapNode.cs
index c866fe00e..3dec30f4a 100644
--- a/src/Discord.Net.Interactions/Map/CommandMapNode.cs
+++ b/src/Discord.Net.Interactions/Map/CommandMapNode.cs
@@ -2,14 +2,13 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
namespace Discord.Interactions
{
internal class CommandMapNode where T : class, ICommandInfo
- {
- private const string RegexWildCardExp = "(\\S+)?";
-
+ {
private readonly string _wildCardStr = "*";
private readonly ConcurrentDictionary> _nodes;
private readonly ConcurrentDictionary _commands;
@@ -35,10 +34,8 @@ namespace Discord.Interactions
{
if (keywords.Count == index + 1)
{
- if (commandInfo.SupportsWildCards && commandInfo.Name.Contains(_wildCardStr))
+ if (commandInfo.SupportsWildCards && RegexUtils.TryBuildRegexPattern(commandInfo, _wildCardStr, out var patternStr))
{
- var escapedStr = RegexUtils.EscapeExcluding(commandInfo.Name, _wildCardStr.ToArray());
- var patternStr = "\\A" + escapedStr.Replace(_wildCardStr, RegexWildCardExp) + "\\Z";
var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled);
if (!_wildCardCommands.TryAdd(regex, commandInfo))
diff --git a/src/Discord.Net.Interactions/Utilities/RegexUtils.cs b/src/Discord.Net.Interactions/Utilities/RegexUtils.cs
index 82ba944f8..b3316106c 100644
--- a/src/Discord.Net.Interactions/Utilities/RegexUtils.cs
+++ b/src/Discord.Net.Interactions/Utilities/RegexUtils.cs
@@ -1,3 +1,4 @@
+using Discord.Interactions;
using System;
using System.Linq;
@@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions
{
return (ch <= '|' && _category[ch] >= E);
}
+
+ internal static int GetWildCardCount(string input, string wildCardExpression)
+ {
+ var escapedWildCard = Regex.Escape(wildCardExpression);
+ var match = Regex.Matches(input, $@"(?(T commandInfo, string wildCardStr, out string pattern) where T: class, ICommandInfo
+ {
+ if (commandInfo.TreatNameAsRegex)
+ {
+ pattern = commandInfo.Name;
+ return true;
+ }
+
+ if (GetWildCardCount(commandInfo.Name, wildCardStr) == 0)
+ {
+ pattern = null;
+ return false;
+ }
+
+ var escapedWildCard = Regex.Escape(wildCardStr);
+ var unquantified = Regex.Replace(commandInfo.Name, $@"(?[^{escapedWildCard}]?)",
+ @"([^\n\t${delimiter}]+)${delimiter}");
+
+ var quantified = Regex.Replace(unquantified, $@"(?[0-9]+)(?,[0-9]*)?(?[^{escapedWildCard}]?)",
+ @"([^\n\t${delimiter}]{${start}${end}})${delimiter}");
+
+ pattern = "\\A" + quantified + "\\Z";
+ return true;
+ }
}
}