backport of REGEX_DEFAULT_MATCH_TIMEOUT (dotnet/corefx#23666)
authorViktor Hofer <viktor.hofer@microsoft.com>
Thu, 7 Sep 2017 20:06:38 +0000 (22:06 +0200)
committerGitHub <noreply@github.com>
Thu, 7 Sep 2017 20:06:38 +0000 (22:06 +0200)
* backport of REGEX_DEFAULT_MATCH_TIMEOUT

* Add more exception tests && improve exception logging

Commit migrated from https://github.com/dotnet/corefx/commit/a6a75b9f2db48137256d2e92cb3df89a9dbf638b

src/libraries/System.Text.RegularExpressions/src/Resources/Strings.resx
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs
src/libraries/System.Text.RegularExpressions/tests/Regex.Ctor.Tests.cs
src/libraries/System.Text.RegularExpressions/tests/Regex.Match.Tests.cs
src/libraries/System.Text.RegularExpressions/tests/Regex.Tests.Common.cs

index e2bb24a..2e23bd0 100644 (file)
@@ -88,6 +88,9 @@
   <data name="IllegalCondition" xml:space="preserve">
     <value>Illegal conditional (?(...)) expression.</value>
   </data>
+  <data name="IllegalDefaultRegexMatchTimeoutInAppDomain" xml:space="preserve">
+    <value>AppDomain data '{0}' contains the invalid value or object '{1}' for specifying a default matching timeout for System.Text.RegularExpressions.Regex.</value>
+  </data>
   <data name="IllegalEndEscape" xml:space="preserve">
     <value>Illegal \\ at end of pattern.</value>
   </data>
   <data name="UnterminatedComment" xml:space="preserve">
     <value>Unterminated (?#...) comment.</value>
   </data>
-</root>
\ No newline at end of file
+</root>
index 9b6c128..ac9a1f0 100644 (file)
@@ -42,9 +42,12 @@ namespace System.Text.RegularExpressions
 
         protected internal TimeSpan internalMatchTimeout;   // timeout for the execution of this regex
 
+        // During static initialisation of Regex we check
+        private const string DefaultMatchTimeout_ConfigKeyName = "REGEX_DEFAULT_MATCH_TIMEOUT";
+
         // DefaultMatchTimeout specifies the match timeout to use if no other timeout was specified
         // by one means or another. Typically, it is set to InfiniteMatchTimeout.
-        internal static readonly TimeSpan DefaultMatchTimeout = InfiniteMatchTimeout;
+        internal static readonly TimeSpan DefaultMatchTimeout = InitDefaultMatchTimeout();
 
         // *********** } match timeout fields ***********
 
@@ -237,6 +240,45 @@ namespace System.Text.RegularExpressions
         }
 
         /// <summary>
+        /// Specifies the default RegEx matching timeout value (i.e. the timeout that will be used if no
+        /// explicit timeout is specified).
+        /// The default is queried from the current <code>AppDomain</code>.
+        /// If the AddDomain's data value for that key is not a <code>TimeSpan</code> value or if it is outside the
+        /// valid range, an exception is thrown.
+        /// If the AddDomain's data value for that key is <code>null</code>, a fallback value is returned.
+        /// </summary>
+        /// <returns>The default RegEx matching timeout for this AppDomain</returns>
+        private static TimeSpan InitDefaultMatchTimeout()
+        {
+            // Query AppDomain
+            AppDomain ad = AppDomain.CurrentDomain;
+            object defaultMatchTimeoutObj = ad.GetData(DefaultMatchTimeout_ConfigKeyName);
+
+            // If no default is specified, use fallback
+            if (defaultMatchTimeoutObj == null)
+            {
+                return InfiniteMatchTimeout;
+            }
+
+            if (defaultMatchTimeoutObj is TimeSpan defaultMatchTimeOut)
+            {
+                // If default timeout is outside the valid range, throw. It will result in a TypeInitializationException:
+                try
+                {
+                    ValidateMatchTimeout(defaultMatchTimeOut);
+                }
+                catch (ArgumentOutOfRangeException)
+                {
+                    throw new ArgumentOutOfRangeException(SR.Format(SR.IllegalDefaultRegexMatchTimeoutInAppDomain, DefaultMatchTimeout_ConfigKeyName, defaultMatchTimeOut));
+                }
+
+                return defaultMatchTimeOut;
+            }
+
+            throw new InvalidCastException(SR.Format(SR.IllegalDefaultRegexMatchTimeoutInAppDomain, DefaultMatchTimeout_ConfigKeyName, defaultMatchTimeoutObj));
+        }
+
+        /// <summary>
         /// Escapes a minimal set of metacharacters (\, *, +, ?, |, {, [, (, ), ^, $, ., #, and
         /// whitespace) by replacing them with their \ codes. This converts a string so that
         /// it can be used as a constant within a regular expression safely. (Note that the
index 16d121e..fc63dae 100644 (file)
@@ -3,13 +3,14 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Runtime.InteropServices;
 using System.Threading;
 using Xunit;
 
 namespace System.Text.RegularExpressions.Tests
 {
-    public class RegexConstructorTests
+    public class RegexConstructorTests : RemoteExecutorTestBase
     {
         public static IEnumerable<object[]> Ctor_TestData()
         {
@@ -76,6 +77,30 @@ namespace System.Text.RegularExpressions.Tests
         }
 
         [Fact]
+        public static void StaticCtor_InvalidTimeoutObject_ExceptionThrown()
+        {
+            RemoteInvoke(() =>
+            {
+                AppDomain.CurrentDomain.SetData(RegexHelpers.DefaultMatchTimeout_ConfigKeyName, true);
+                Assert.Throws<TypeInitializationException>(() => Regex.InfiniteMatchTimeout);
+
+                return SuccessExitCode;
+            });
+        }
+
+        [Fact]
+        public static void StaticCtor_InvalidTimeoutRange_ExceptionThrown()
+        {
+            RemoteInvoke(() =>
+            {
+                AppDomain.CurrentDomain.SetData(RegexHelpers.DefaultMatchTimeout_ConfigKeyName, TimeSpan.Zero);
+                Assert.Throws<TypeInitializationException>(() => Regex.InfiniteMatchTimeout);
+
+                return SuccessExitCode;
+            });
+        }
+
+        [Fact]
         public void CacheSize_Get()
         {
             Assert.Equal(15, Regex.CacheSize);
index 43d51f0..03ffcca 100644 (file)
@@ -328,6 +328,21 @@ namespace System.Text.RegularExpressions.Tests
             Assert.Equal("a", match.Value);
         }
 
+        [Fact]
+        public void Match_Timeout_Throws()
+        {
+            RemoteInvoke(() =>
+            {
+                const string Pattern = @"^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$";
+                string input = new string('a', 50) + "@a.a";
+
+                AppDomain.CurrentDomain.SetData(RegexHelpers.DefaultMatchTimeout_ConfigKeyName, TimeSpan.FromMilliseconds(100));
+                Assert.Throws<RegexMatchTimeoutException>(() => new Regex(Pattern).Match(input));
+
+                return SuccessExitCode;
+            });
+        }
+
         public static IEnumerable<object[]> Match_Advanced_TestData()
         {
             // \B special character escape: ".*\\B(SUCCESS)\\B.*"
@@ -596,7 +611,7 @@ namespace System.Text.RegularExpressions.Tests
             bool isDefaultCount = RegexHelpers.IsDefaultStart(input, options, length);
             if (options == RegexOptions.None)
             {
-                if (isDefaultStart  && isDefaultCount)
+                if (isDefaultStart && isDefaultCount)
                 {
                     // Use Match(string) or Match(string, string)
                     VerifyMatch(new Regex(pattern).Match(input), true, expected);
index 0ec9c44..45d8718 100644 (file)
@@ -6,6 +6,8 @@ namespace System.Text.RegularExpressions.Tests
 {
     public static class RegexHelpers
     {
+        public const string DefaultMatchTimeout_ConfigKeyName = "REGEX_DEFAULT_MATCH_TIMEOUT";
+
         public static bool IsDefaultCount(string input, RegexOptions options, int count)
         {
             if ((options & RegexOptions.RightToLeft) != 0)