Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Regex handling

- For fixed patterns on modern targets, use source-generated regexes with explicit match timeouts (`GeneratedRegex` under `NET8_0_OR_GREATER`).
- Use conditional compilation to provide a regular `Regex` with the same pattern and an explicit match timeout for older targets. Do not duplicate regex validation with a manually maintained character scanner.
- Older-runtime regex matching may still time out under load because timeouts use wall-clock time. If consumers encounter this limitation, recommend upgrading to a modern runtime that uses the source-generated implementation.
- Keep shared patterns in constants and reference those constants in validation diagnostics and tests.
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ namespace Microsoft.OpenApi
/// The validation rules for <see cref="OpenApiComponents"/>.
/// </summary>
[OpenApiRule]
public static class OpenApiComponentsRules
public static partial class OpenApiComponentsRules
{
/// <summary>
/// The key regex.
/// The key regex pattern.
/// </summary>
internal static readonly Regex KeyRegex = new(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
internal const string KeyPattern = @"^[a-zA-Z0-9\.\-_]+$";

#if NET8_0_OR_GREATER
[GeneratedRegex(KeyPattern, RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex KeyRegex();
#else
private static readonly Regex KeyRegex = new(KeyPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100));
Comment thread
baywet marked this conversation as resolved.
Resolved
#endif

/// <summary>
/// All the fixed fields declared above are objects
Expand Down Expand Up @@ -54,12 +61,18 @@ private static void ValidateKeys(IValidationContext context, IEnumerable<string>

foreach (var key in keys)
{
if (!KeyRegex.IsMatch(key))
#if NET8_0_OR_GREATER
var isValidKey = KeyRegex().IsMatch(key);
#else
var isValidKey = KeyRegex.IsMatch(key);
#endif
if (!isValidKey)
{
context.CreateError(nameof(KeyMustBeRegularExpression),
string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyRegex.ToString()));
string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyPattern));
}
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
// Licensed under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.OpenApi.Validations.Tests
Expand Down Expand Up @@ -32,8 +36,65 @@ public void ValidateKeyMustMatchRegularExpressionInComponents()
Assert.False(result);
Assert.NotNull(errors);
var error = Assert.Single(errors);
Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", OpenApiComponentsRules.KeyRegex.ToString()),
Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", OpenApiComponentsRules.KeyPattern),
error.Message);
}

[Theory]
[InlineData("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_", true)]
[InlineData("", false)]
[InlineData("a b", false)]
[InlineData("a/b", false)]
[InlineData("é", false)]
[InlineData("12", false)]
[InlineData("a\n", true)]
[InlineData("\n", false)]
[InlineData("a\n\n", false)]
[InlineData("a\r\n", false)]
[InlineData("a\nb", false)]
[InlineData("a\0", false)]
public void ValidateComponentKeyPreservesRegexBehavior(string key, bool isValid)
{
var components = new OpenApiComponents
{
Schemas = new Dictionary<string, IOpenApiSchema>
{
{ key, new OpenApiSchema() }
}
};

var rules = new ValidationRuleSet();
rules.Add(typeof(OpenApiComponents), OpenApiComponentsRules.KeyMustBeRegularExpression);
var errors = components.Validate(rules);

Assert.Equal(isValid, !errors.Any());
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task LoadAsyncValidatesLongComponentKeys(bool isValid)
Comment thread
baywet marked this conversation as resolved.
Resolved
{
var key = new string('a', 1_000_000) + (isValid ? string.Empty : "!");
var json = """
{"openapi":"3.1.0","info":{"title":"Test","version":"1.0"},"paths":{},"components":{"schemas":{
""" + JsonSerializer.Serialize(key) + ":{\"type\":\"string\"}}}}";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));

var result = await OpenApiDocument.LoadAsync(stream, cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result.Document);
Assert.NotNull(result.Diagnostic);
if (isValid)
{
Assert.Empty(result.Diagnostic.Errors);
}
else
{
var error = Assert.Single(result.Diagnostic.Errors);
Assert.Equal(string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr,
key, "schemas", OpenApiComponentsRules.KeyPattern), error.Message);
}
}
}
}
Loading