Skip to content

Commit 25d2d3b

Browse files
authored
Simplify package ID regex (#8453)
Also refactors some regex helpers.
1 parent a79b0f0 commit 25d2d3b

File tree

12 files changed

+142
-73
lines changed

12 files changed

+142
-73
lines changed
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Text.RegularExpressions;
6+
7+
namespace NuGetGallery
8+
{
9+
public static class RegexEx
10+
{
11+
// This timeout must be short enough to prevent runaway regular expressions,
12+
// but long enough to prevent reliability issues across all our regular expressions.
13+
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(15);
14+
15+
/// <summary>
16+
/// Creates a new instance of the <see cref="Regex"/> class with a default timeout configured
17+
/// for the pattern matching method to attempt a match.
18+
/// </summary>
19+
/// <param name="pattern">The regular expression pattern to match.</param>
20+
/// <param name="options">A bitwise combiantion of the enumeration values that modify the expression.</param>
21+
/// <returns>A regular expression instance that can be used to match inputs.</returns>
22+
public static Regex CreateWithTimeout(string pattern, RegexOptions options)
23+
{
24+
return new Regex(pattern, options, Timeout);
25+
}
26+
27+
/// <summary>
28+
/// In a specific input string, replaces all substrings that match a specified regular expression.
29+
/// Throws a <see cref="RegexMatchTimeoutException"/> if the timeout is reached.
30+
/// </summary>
31+
/// <param name="input">The string to search for matches.</param>
32+
/// <param name="pattern">The regular expression pattern to match.</param>
33+
/// <param name="evaluator">The handler to replace matches.</param>
34+
/// <param name="options">A bitwise combination that provide options for matching.</param>
35+
/// <returns>A new string with the matches replaced.</returns>
36+
/// <exception cref="RegexMatchTimeoutException">Thrown if the matches exceed the default timeout.</exception>
37+
public static string ReplaceWithTimeout(
38+
string input,
39+
string pattern,
40+
string replacement,
41+
RegexOptions options)
42+
{
43+
return Regex.Replace(input, pattern, replacement, options, Timeout);
44+
}
45+
46+
/// <summary>
47+
/// In a specific input string, replaces all substrings that match a specified regular expression.
48+
/// </summary>
49+
/// <param name="input">The string to search for matches.</param>
50+
/// <param name="pattern">The regular expression pattern to match.</param>
51+
/// <param name="evaluator">The handler to replace matches.</param>
52+
/// <param name="options">A bitwise combination that provide options for matching.</param>
53+
/// <returns>A new string with the matches replaced, or the original string if the matches timeout.</returns>
54+
public static string ReplaceWithTimeoutOrOriginal(
55+
string input,
56+
string pattern,
57+
MatchEvaluator evaluator,
58+
RegexOptions options)
59+
{
60+
try
61+
{
62+
return Regex.Replace(input, pattern, evaluator, options, Timeout);
63+
}
64+
catch (RegexMatchTimeoutException)
65+
{
66+
return input;
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Searches the input string for the first occurrence of the specified regular expression,
72+
/// using the specified matching options and the default time-out interval.
73+
/// </summary>
74+
/// <param name="input">The string to search for a match.</param>
75+
/// <param name="pattern">The regular expression pattern to match.</param>
76+
/// <param name="options">A bitwise combination of the enumeration values that provide options for matching.</param>
77+
/// <returns>An object that contains information about the match, or <c>null</c> if and only if the match timed out.</returns>
78+
public static Match MatchWithTimeoutOrNull(
79+
string input,
80+
string pattern,
81+
RegexOptions options)
82+
{
83+
try
84+
{
85+
return Regex.Match(input, pattern, options, Timeout);
86+
}
87+
catch (RegexMatchTimeoutException)
88+
{
89+
return null;
90+
}
91+
}
92+
93+
/// <summary>
94+
/// Searches the input string for all occurrence of the specified regular expression,
95+
/// using the specified matching options and the default time-out interval.
96+
/// </summary>
97+
/// <param name="input">The string to search for a match.</param>
98+
/// <param name="pattern">The regular expression pattern to match.</param>
99+
/// <param name="options">A bitwise combination of the enumeration values that provide options for matching.</param>
100+
/// <returns>
101+
/// A collection of the matches found by the search.
102+
/// If no matches are found, the method returns an empty collection.
103+
/// If and only if the matches timeout, returns <c>null</c>.</returns>
104+
public static MatchCollection MatchesWithTimeoutOrNull(
105+
string input,
106+
string pattern,
107+
RegexOptions options)
108+
{
109+
try
110+
{
111+
return Regex.Matches(input, pattern, options, Timeout);
112+
}
113+
catch (RegexMatchTimeoutException)
114+
{
115+
return null;
116+
}
117+
}
118+
}
119+
}

src/NuGetGallery.Core/NuGetVersionExtensions.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ public static string ToFullString(string version)
3636
public static class NuGetVersionExtensions
3737
{
3838
private const RegexOptions SemanticVersionRegexFlags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;
39-
private static readonly Regex SemanticVersionRegex = new Regex(@"^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[a-z][0-9a-z-]*)?$", SemanticVersionRegexFlags);
39+
private static readonly Regex SemanticVersionRegex = RegexEx.CreateWithTimeout(
40+
@"^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[a-z][0-9a-z-]*)?$",
41+
SemanticVersionRegexFlags);
4042

4143
public static string ToNormalizedStringSafe(this NuGetVersion self)
4244
{

src/NuGetGallery.Core/Packaging/PackageIdValidator.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ namespace NuGetGallery.Packaging
1010
{
1111
public static class PackageIdValidator
1212
{
13-
private static readonly Regex IdRegex = new Regex(@"^\w+([_.-]\w+)*$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
13+
private static readonly Regex IdRegex = RegexEx.CreateWithTimeout(
14+
@"^\w+([.-]\w+)*$",
15+
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
1416

1517
public static bool IsValidPackageId(string packageId)
1618
{

src/NuGetGallery.Services/Authentication/AsyncFileUpload/AsyncFileUploadRequestParser.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
35
using System.Collections.Generic;
46
using System.Diagnostics;
57
using System.Text;
@@ -173,8 +175,8 @@ private string ParseContentDisposition(byte[] buffer)
173175
//
174176
// We want to extract the file name out of it.
175177
string content = _encoding.GetString(buffer);
176-
var match = Regex.Match(content, @"filename\=""(.*)\""");
177-
if (match.Success && match.Groups.Count > 1)
178+
var match = RegexEx.MatchWithTimeoutOrNull(content, @"filename\=""(.*)\""", RegexOptions.None);
179+
if (match != null && match.Success && match.Groups.Count > 1)
178180
{
179181
string filename = match.Groups[1].Value;
180182
return filename;

src/NuGetGallery.Services/Authentication/NuGetPackagePattern.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static class NuGetPackagePattern
1717
/// <returns><c>true</c> if the string matches the given pattern; otherwise <c>false</c>.</returns>
1818
public static bool MatchesPackagePattern(this string str, string globPattern)
1919
{
20-
return new Regex(
20+
return RegexEx.CreateWithTimeout(
2121
"^" + Regex.Escape(globPattern).Replace(@"\*", ".*") + "$",
2222
RegexOptions.IgnoreCase | RegexOptions.Singleline
2323
).IsMatch(str);

src/NuGetGallery.Services/Authentication/Providers/Authenticator.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ namespace NuGetGallery.Authentication.Providers
1616
public abstract class Authenticator
1717
{
1818
public const string AuthPrefix = "Auth.";
19-
private static readonly Regex NameShortener = new Regex(@"^(?<shortname>[A-Za-z0-9_]*)Authenticator$");
19+
private static readonly Regex NameShortener = RegexEx.CreateWithTimeout(
20+
@"^(?<shortname>[A-Za-z0-9_]*)Authenticator$",
21+
RegexOptions.None);
2022

2123
public AuthenticatorConfiguration BaseConfig { get; private set; }
2224

src/NuGetGallery.Services/PackageManagement/ReservedNamespaceService.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ namespace NuGetGallery
1515
{
1616
public class ReservedNamespaceService : IReservedNamespaceService
1717
{
18-
private static readonly Regex NamespaceRegex = new Regex(@"^\w+([_.-]\w+)*[.-]?$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
18+
private static readonly Regex NamespaceRegex = RegexEx.CreateWithTimeout(
19+
@"^\w+([.-]\w+)*[.-]?$",
20+
RegexOptions.Compiled | RegexOptions.ExplicitCapture);
1921

2022
public IEntitiesContext EntitiesContext { get; protected set; }
2123
public IEntityRepository<ReservedNamespace> ReservedNamespaceRepository { get; protected set; }

src/NuGetGallery/Helpers/HtmlExtensions.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ void appendText(StringBuilder builder, string inputText)
6969
encodedText = encodedText.Replace("\n", "<br />");
7070

7171
// Replace more than one space in a row with a space then &nbsp;.
72-
encodedText = RegexEx.TryReplaceWithTimeout(
72+
encodedText = RegexEx.ReplaceWithTimeoutOrOriginal(
7373
encodedText,
7474
" +",
7575
match => " " + string.Join(string.Empty, Enumerable.Repeat("&nbsp;", match.Value.Length - 1)),
@@ -101,7 +101,7 @@ void appendUrl(StringBuilder builder, string inputText)
101101
string siteRoot = configurationService.GetSiteRoot(useHttps: true);
102102

103103
// Format links to NuGet packages
104-
Match packageMatch = RegexEx.MatchWithTimeout(
104+
Match packageMatch = RegexEx.MatchWithTimeoutOrNull(
105105
formattedUri,
106106
$@"({Regex.Escape(siteRoot)}\/packages\/(?<name>\w+([_.-]\w+)*(\/[0-9a-zA-Z-.]+)?)\/?$)",
107107
RegexOptions.IgnoreCase);
@@ -124,7 +124,7 @@ void appendUrl(StringBuilder builder, string inputText)
124124

125125
// Turn HTTP and HTTPS URLs into links.
126126
// Source: https://stackoverflow.com/a/4750468
127-
var matches = RegexEx.MatchesWithTimeout(
127+
var matches = RegexEx.MatchesWithTimeoutOrNull(
128128
text,
129129
@"((http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?)",
130130
RegexOptions.IgnoreCase);

src/NuGetGallery/Helpers/RegexEx.cs

-59
This file was deleted.

src/NuGetGallery/NuGetGallery.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,6 @@
362362
<Compile Include="Helpers\StreamHelper.cs" />
363363
<Compile Include="Helpers\TextHelper.cs" />
364364
<Compile Include="Helpers\ZipArchiveHelpers.cs" />
365-
<Compile Include="Helpers\RegexEx.cs" />
366365
<Compile Include="Helpers\RouteUrlTemplate.cs" />
367366
<Compile Include="Infrastructure\Lucene\HttpClientWrapper.cs" />
368367
<Compile Include="Infrastructure\Lucene\IResilientSearchClient.cs" />

src/NuGetGallery/Services/ReadMeService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ private static async Task<string> ReadMaxAsync(Stream stream, int maxSize, Encod
320320
return encoding.GetString(buffer).Trim('\0');
321321
}
322322

323-
private static readonly Regex NewLineRegex = new Regex(@"\n|\r\n");
323+
private static readonly Regex NewLineRegex = RegexEx.CreateWithTimeout(@"\n|\r\n", RegexOptions.None);
324324

325325
private static string NormalizeNewLines(string content)
326326
{

src/NuGetGallery/Services/TyposquattingDistanceCalculation.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public static bool IsDistanceLessThanThreshold(string str1, string str2, int thr
4141
throw new ArgumentNullException(nameof(str2));
4242
}
4343

44-
var newStr1 = Regex.Replace(str1, SpecialCharactersToString, string.Empty);
45-
var newStr2 = Regex.Replace(str2, SpecialCharactersToString, string.Empty);
44+
var newStr1 = RegexEx.ReplaceWithTimeout(str1, SpecialCharactersToString, string.Empty, RegexOptions.None);
45+
var newStr2 = RegexEx.ReplaceWithTimeout(str2, SpecialCharactersToString, string.Empty, RegexOptions.None);
4646
if (Math.Abs(newStr1.Length - newStr2.Length) > threshold)
4747
{
4848
return false;

0 commit comments

Comments
 (0)