Browse Source

implement wildcard lenght quantifiers, TreatAsRegex property and solve catastrpohic backtracking (#2528)

tags/3.9.0
Cenk Ergen GitHub 2 years ago
parent
commit
3b107c2d01
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 89 additions and 7 deletions
  1. +6
    -0
      src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs
  2. +5
    -0
      src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs
  3. +20
    -0
      src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs
  4. +14
    -0
      src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs
  5. +3
    -1
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  6. +3
    -0
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  7. +2
    -0
      src/Discord.Net.Interactions/Info/ICommandInfo.cs
  8. +3
    -6
      src/Discord.Net.Interactions/Map/CommandMapNode.cs
  9. +33
    -0
      src/Discord.Net.Interactions/Utilities/RegexUtils.cs

+ 6
- 0
src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Runtime.CompilerServices;


namespace Discord.Interactions namespace Discord.Interactions
{ {
@@ -28,6 +29,11 @@ namespace Discord.Interactions
/// </summary> /// </summary>
public RunMode RunMode { get; } public RunMode RunMode { get; }


/// <summary>
/// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern.
/// </summary>
public bool TreatAsRegex { get; set; } = false;

/// <summary> /// <summary>
/// Create a command for component interaction handling. /// Create a command for component interaction handling.
/// </summary> /// </summary>


+ 5
- 0
src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs View File

@@ -28,6 +28,11 @@ namespace Discord.Interactions
/// </summary> /// </summary>
public RunMode RunMode { get; } public RunMode RunMode { get; }


/// <summary>
/// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern.
/// </summary>
public bool TreatAsRegex { get; set; } = false;

/// <summary> /// <summary>
/// Create a command for modal interaction handling. /// Create a command for modal interaction handling.
/// </summary> /// </summary>


+ 20
- 0
src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs View File

@@ -35,6 +35,9 @@ namespace Discord.Interactions.Builders
/// <inheritdoc/> /// <inheritdoc/>
public bool IgnoreGroupNames { get; set; } public bool IgnoreGroupNames { get; set; }


/// <inheritdoc/>
public bool TreatNameAsRegex { get; set; }

/// <inheritdoc/> /// <inheritdoc/>
public RunMode RunMode { get; set; } public RunMode RunMode { get; set; }


@@ -117,6 +120,19 @@ namespace Discord.Interactions.Builders
return Instance; return Instance;
} }


/// <summary>
/// Sets <see cref="TreatNameAsRegex"/>.
/// </summary>
/// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithNameAsRegex (bool value)
{
TreatNameAsRegex = value;
return Instance;
}

/// <summary> /// <summary>
/// Adds parameter builders to <see cref="Parameters"/>. /// Adds parameter builders to <see cref="Parameters"/>.
/// </summary> /// </summary>
@@ -163,6 +179,10 @@ namespace Discord.Interactions.Builders
ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) => ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) =>
SetRunMode(runMode); SetRunMode(runMode);


/// <inheritdoc/>
ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) =>
WithNameAsRegex(value);

/// <inheritdoc/> /// <inheritdoc/>
ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) =>
AddParameters(parameters as TParamBuilder); AddParameters(parameters as TParamBuilder);


+ 14
- 0
src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs View File

@@ -34,6 +34,11 @@ namespace Discord.Interactions.Builders
/// </summary> /// </summary>
bool IgnoreGroupNames { get; set; } bool IgnoreGroupNames { get; set; }


/// <summary>
/// Gets or sets whether the <see cref="Name"/> should be directly used as a Regex pattern.
/// </summary>
bool TreatNameAsRegex { get; set; }

/// <summary> /// <summary>
/// Gets or sets the run mode this command gets executed with. /// Gets or sets the run mode this command gets executed with.
/// </summary> /// </summary>
@@ -90,6 +95,15 @@ namespace Discord.Interactions.Builders
/// </returns> /// </returns>
ICommandBuilder SetRunMode (RunMode runMode); ICommandBuilder SetRunMode (RunMode runMode);


/// <summary>
/// Sets <see cref="TreatNameAsRegex"/>.
/// </summary>
/// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder WithNameAsRegex(bool value);

/// <summary> /// <summary>
/// Adds parameter builders to <see cref="Parameters"/>. /// Adds parameter builders to <see cref="Parameters"/>.
/// </summary> /// </summary>


+ 3
- 1
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

@@ -274,6 +274,7 @@ namespace Discord.Interactions.Builders
builder.Name = interaction.CustomId; builder.Name = interaction.CustomId;
builder.RunMode = interaction.RunMode; builder.RunMode = interaction.RunMode;
builder.IgnoreGroupNames = interaction.IgnoreGroupNames; builder.IgnoreGroupNames = interaction.IgnoreGroupNames;
builder.TreatNameAsRegex = interaction.TreatAsRegex;
} }
break; break;
case PreconditionAttribute precondition: case PreconditionAttribute precondition:
@@ -287,7 +288,7 @@ namespace Discord.Interactions.Builders


var parameters = methodInfo.GetParameters(); 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) foreach (var parameter in parameters)
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount));
@@ -355,6 +356,7 @@ namespace Discord.Interactions.Builders
builder.Name = modal.CustomId; builder.Name = modal.CustomId;
builder.RunMode = modal.RunMode; builder.RunMode = modal.RunMode;
builder.IgnoreGroupNames = modal.IgnoreGroupNames; builder.IgnoreGroupNames = modal.IgnoreGroupNames;
builder.TreatNameAsRegex = modal.TreatAsRegex;
} }
break; break;
case PreconditionAttribute precondition: case PreconditionAttribute precondition:


+ 3
- 0
src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs View File

@@ -66,6 +66,8 @@ namespace Discord.Interactions
/// <inheritdoc cref="ICommandInfo.Parameters"/> /// <inheritdoc cref="ICommandInfo.Parameters"/>
public abstract IReadOnlyList<TParameter> Parameters { get; } public abstract IReadOnlyList<TParameter> Parameters { get; }


public bool TreatNameAsRegex { get; }

internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService)
{ {
CommandService = commandService; CommandService = commandService;
@@ -78,6 +80,7 @@ namespace Discord.Interactions
RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode;
Attributes = builder.Attributes.ToImmutableArray(); Attributes = builder.Attributes.ToImmutableArray();
Preconditions = builder.Preconditions.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray();
TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards;


_action = builder.Callback; _action = builder.Callback;
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);


+ 2
- 0
src/Discord.Net.Interactions/Info/ICommandInfo.cs View File

@@ -65,6 +65,8 @@ namespace Discord.Interactions
/// </summary> /// </summary>
IReadOnlyCollection<IParameterInfo> Parameters { get; } IReadOnlyCollection<IParameterInfo> Parameters { get; }


bool TreatNameAsRegex { get; }

/// <summary> /// <summary>
/// Executes the command with the provided context. /// Executes the command with the provided context.
/// </summary> /// </summary>


+ 3
- 6
src/Discord.Net.Interactions/Map/CommandMapNode.cs View File

@@ -2,14 +2,13 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;


namespace Discord.Interactions namespace Discord.Interactions
{ {
internal class CommandMapNode<T> where T : class, ICommandInfo internal class CommandMapNode<T> where T : class, ICommandInfo
{
private const string RegexWildCardExp = "(\\S+)?";

{
private readonly string _wildCardStr = "*"; private readonly string _wildCardStr = "*";
private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes; private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes;
private readonly ConcurrentDictionary<string, T> _commands; private readonly ConcurrentDictionary<string, T> _commands;
@@ -35,10 +34,8 @@ namespace Discord.Interactions
{ {
if (keywords.Count == index + 1) 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); var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled);


if (!_wildCardCommands.TryAdd(regex, commandInfo)) if (!_wildCardCommands.TryAdd(regex, commandInfo))


+ 33
- 0
src/Discord.Net.Interactions/Utilities/RegexUtils.cs View File

@@ -1,3 +1,4 @@
using Discord.Interactions;
using System; using System;
using System.Linq; using System.Linq;


@@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions
{ {
return (ch <= '|' && _category[ch] >= E); return (ch <= '|' && _category[ch] >= E);
} }

internal static int GetWildCardCount(string input, string wildCardExpression)
{
var escapedWildCard = Regex.Escape(wildCardExpression);
var match = Regex.Matches(input, $@"(?<!\\){escapedWildCard}|(?<!\\){{[0-9]+(?:,[0-9]*)?(?<!\\)}}");
return match.Count;
}

internal static bool TryBuildRegexPattern<T>(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}(?<delimiter>[^{escapedWildCard}]?)",
@"([^\n\t${delimiter}]+)${delimiter}");

var quantified = Regex.Replace(unquantified, $@"(?<!\\){{(?<start>[0-9]+)(?<end>,[0-9]*)?(?<!\\)}}(?<delimiter>[^{escapedWildCard}]?)",
@"([^\n\t${delimiter}]{${start}${end}})${delimiter}");

pattern = "\\A" + quantified + "\\Z";
return true;
}
} }
} }

Loading…
Cancel
Save