Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
72 changes: 71 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public abstract class OpenApiExtensibleDictionary<T> : Dictionary<string, T>,
/// <summary>
/// Parameterless constructor
/// </summary>
protected OpenApiExtensibleDictionary():this([]) { }
protected OpenApiExtensibleDictionary() : this([]) { }
/// <summary>
/// Initializes a copy of <see cref="OpenApiExtensibleDictionary{T}"/> class.
/// </summary>
Expand Down Expand Up @@ -71,6 +71,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
{
Utils.CheckArgumentNull(writer);

ValidatePathTemplateOperators(version);

writer.WriteStartObject();

foreach (var item in this)
Expand All @@ -83,13 +85,81 @@ 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}");
}
}
}

/// <summary>
/// Determines whether the given path contains any RFC 6570 operators.
/// </summary>
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;
}

/// <summary>
/// Serialize to Open Api v2.0
/// </summary>
public void SerializeAsV2(IOpenApiWriter writer)
{
Utils.CheckArgumentNull(writer);

ValidatePathTemplateOperators(OpenApiSpecVersion.OpenApi2_0);

writer.WriteStartObject();

foreach (var item in this)
Expand Down
70 changes: 70 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs
Original file line number Diff line number Diff line change
@@ -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<OpenApiException>(() => paths.SerializeAsV3(writer));
Assert.Throws<OpenApiException>(() => paths.SerializeAsV31(writer));
Assert.Throws<OpenApiException>(() => 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<OpenApiException>(() => paths.SerializeAsV3(writer));
Assert.Throws<OpenApiException>(() => paths.SerializeAsV31(writer));
Assert.Throws<OpenApiException>(() => 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);
}
}
}