diff --git a/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs b/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs index 966516eb4..cf9e7e8f4 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs @@ -18,7 +18,7 @@ public abstract class OpenApiExtensibleDictionary : Dictionary, /// /// Parameterless constructor /// - protected OpenApiExtensibleDictionary():this([]) { } + protected OpenApiExtensibleDictionary() : this([]) { } /// /// Initializes a copy of class. /// @@ -71,6 +71,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version { Utils.CheckArgumentNull(writer); + ValidatePathTemplateOperators(version); + writer.WriteStartObject(); foreach (var item in this) @@ -83,6 +85,72 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version writer.WriteEndObject(); } + private static readonly char[] Rfc6570Operators = ['+', '#', '.', '/', ';', '?', '&']; + + private void ValidatePathTemplateOperators(OpenApiSpecVersion version) + { + if (version >= OpenApiSpecVersion.OpenApi3_2 || this is not OpenApiPaths) + { + return; + } + + foreach (var path in Keys) + { + if (ContainsRfc6570Operator(path)) + { + throw new OpenApiException($"RFC 6570 URI template operators are only supported in OpenAPI 3.2 and later versions. Path: '{path}'. Current version: {version}"); + } + } + } + + /// + /// Determines whether the given path contains any RFC 6570 operators. + /// + private static bool ContainsRfc6570Operator(string path) + { + if (path.Length == 0) + { + return false; + } + + var template = path; + + var startIndex = 0; + while (true) + { + var open = template.IndexOf('{', startIndex); + if (open < 0) + { + break; + } + + var close = template.IndexOf('}', open + 1); + if (close < 0) + { + break; + } + + var expression = template.Substring(open + 1, close - open - 1); + if (!string.IsNullOrEmpty(expression)) + { + var first = expression[0]; + if (Array.IndexOf(Rfc6570Operators, first) >= 0) + { + return true; + } + + if (expression.IndexOf('*') >= 0) + { + return true; + } + } + + startIndex = close + 1; + } + + return false; + } + /// /// Serialize to Open Api v2.0 /// @@ -90,6 +158,8 @@ public void SerializeAsV2(IOpenApiWriter writer) { Utils.CheckArgumentNull(writer); + ValidatePathTemplateOperators(OpenApiSpecVersion.OpenApi2_0); + writer.WriteStartObject(); foreach (var item in this) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs new file mode 100644 index 000000000..033e0bd37 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Models +{ + [Collection("DefaultSettings")] + public class OpenApiPathsTests + { + [Fact] + public void SerializePaths_WithRfc6570Operator_BelowV32_Throws() + { + var paths = new OpenApiPaths + { + ["/files/{+path}"] = new OpenApiPathItem() + }; + + var writer = new OpenApiJsonWriter(new StringWriter()); + + Assert.Throws(() => paths.SerializeAsV3(writer)); + Assert.Throws(() => paths.SerializeAsV31(writer)); + Assert.Throws(() => paths.SerializeAsV2(writer)); + } + + [Fact] + public void SerializePaths_WithExplodeOperator_BelowV32_Throws() + { + var paths = new OpenApiPaths + { + ["/files/{path*}"] = new OpenApiPathItem() + }; + + var writer = new OpenApiJsonWriter(new StringWriter()); + + Assert.Throws(() => paths.SerializeAsV3(writer)); + Assert.Throws(() => paths.SerializeAsV31(writer)); + Assert.Throws(() => paths.SerializeAsV2(writer)); + } + + [Fact] + public void SerializePaths_WithRfc6570Operator_V32_Succeeds() + { + var paths = new OpenApiPaths + { + ["/files/{+path}"] = new OpenApiPathItem() + }; + + var writer = new OpenApiJsonWriter(new StringWriter()); + + paths.SerializeAsV32(writer); + } + + [Fact] + public void SerializePaths_WithoutOperators_BelowV32_Succeeds() + { + var paths = new OpenApiPaths + { + ["/files/{path}"] = new OpenApiPathItem() + }; + + var writer = new OpenApiJsonWriter(new StringWriter()); + + paths.SerializeAsV3(writer); + paths.SerializeAsV31(writer); + paths.SerializeAsV2(writer); + } + } +}