From 6dac23b96069119e73d8514bb921c2ca708df915 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 17 Mar 2026 15:08:11 -0400 Subject: [PATCH 1/6] draft: fix location key encoding Signed-off-by: Vincent Biret --- .../Services/OpenApiVisitorBaseTests.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs new file mode 100644 index 000000000..ce92819f3 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Services; + +public class OpenApiVisitorBaseTests +{ + [Fact] + public void EncodesReservedCharacters() + { + // Given + var openApiDocument = new OpenApiDocument + { + Info = new() + { + Title = "foo", + Version = "1.2.2" + }, + Paths = new() + { + }, + Components = new() + { + Schemas = new Dictionary() + { + ["Pet~"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object + }, + ["Pet/"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object + }, + } + } + }; + var visitor = new LocatorVisitor(); + + // When + visitor.Visit(openApiDocument); + + // Then + Assert.Equivalent( + new List + { + "#/components/schemas/Pet~0", + "#/components/schemas/Pet~1" + }, visitor.Locations); + } + + private class LocatorVisitor : OpenApiVisitorBase + { + public List Locations { get; } = new List(); + + public override void Visit(IOpenApiSchema openApiSchema) + { + Locations.Add(this.PathString); + } + public override void Visit(OpenApiComponents components) + { + Enter("schemas"); + if (components.Schemas != null) + { + foreach (var schemaKvp in components.Schemas) + { + Enter(schemaKvp.Key); + this.Visit(schemaKvp.Value); + Exit(); + } + } + Exit(); + } + public override void Visit(OpenApiDocument doc) + { + Enter("components"); + Visit(doc.Components); + Exit(); + } + } +} From 4c757e1e9806f95ed598f349e4fc35a0307b9342 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 17 Mar 2026 16:09:29 -0400 Subject: [PATCH 2/6] fix: encoding of special characters for JSON paths Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 397062aca..15010c0e5 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Text.Json.Nodes; @@ -27,7 +28,11 @@ public abstract class OpenApiVisitorBase /// Identifier for context public virtual void Enter(string segment) { - this._path.Push(segment); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER + this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase)); +#else + this._path.Push(segment.Replace("~", "~0").Replace("/", "~1")); +#endif } /// @@ -41,7 +46,7 @@ public virtual void Exit() /// /// Pointer to source of validation error in document /// - public string PathString { get => "#/" + String.Join("/", _path.Reverse()); } + public string PathString { get => "#/" + string.Join("/", _path.Reverse()); } /// /// Visits From 471a61a2567bb517ede94f0bec1a53ef806b2db3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 17 Mar 2026 16:14:18 -0400 Subject: [PATCH 3/6] fix: potential double encoding of paths Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiWalker.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index c8e5d72d7..01172cdd1 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -1297,15 +1297,6 @@ internal void Walk(IOpenApiElement element) } } - private static string ReplaceSlashes(string value) - { -#if NET8_0_OR_GREATER - return value.Replace("/", "~1", StringComparison.Ordinal); -#else - return value.Replace("/", "~1"); -#endif - } - /// /// Adds a segment to the context path to enable pointing to the current location in the document /// @@ -1315,7 +1306,7 @@ private static string ReplaceSlashes(string value) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk) { - _visitor.Enter(ReplaceSlashes(context)); + _visitor.Enter(context); walk(this, state); _visitor.Exit(); } @@ -1330,7 +1321,7 @@ private void WalkItem(string context, T state, Action walk) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk, bool isComponent) { - _visitor.Enter(ReplaceSlashes(context)); + _visitor.Enter(context); walk(this, state, isComponent); _visitor.Exit(); } @@ -1351,7 +1342,7 @@ private void WalkDictionary( { if (state != null && state.Count > 0) { - _visitor.Enter(ReplaceSlashes(context)); + _visitor.Enter(context); foreach (var item in state) { From 6e16cbc84b7bab8438d3876b49e145a9af5c1760 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 17 Mar 2026 16:14:28 -0400 Subject: [PATCH 4/6] fix null reference exception Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 15010c0e5..45a85d3ab 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -28,6 +28,11 @@ public abstract class OpenApiVisitorBase /// Identifier for context public virtual void Enter(string segment) { + if (string.IsNullOrEmpty(segment)) + { + this._path.Push(string.Empty); + return; + } #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase)); #else From b246cd0bafde78892aa22e6ffe046cf5a438eb7d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 17 Mar 2026 16:34:03 -0400 Subject: [PATCH 5/6] fix: double encoding of json pointer for invalid reference rule Signed-off-by: Vincent Biret --- .../Validations/Rules/OpenApiDocumentRules.cs | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index a1f166ee3..38767035f 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.OpenApi { @@ -74,28 +76,49 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference) { if (reference.RecursiveTarget is null) { + var segments = GetSegments().ToArray(); + EnterSegments(segments); // The reference was not followed to a valid schema somewhere in the document - context.Enter(GetSegment()); context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, reference.Reference.ReferenceV3)); - context.Exit(); + ExitSegments(segments.Length); } } catch (InvalidOperationException ex) { - context.Enter(GetSegment()); + var segments = GetSegments().ToArray(); + EnterSegments(segments); context.CreateWarning(ruleName, ex.Message); - context.Exit(); + ExitSegments(segments.Length); } - string GetSegment() + void ExitSegments(int length) { - // Trim off the leading "#/" as the context is already at the root of the document - return -#if NET8_0_OR_GREATER - $"{PathString[2..]}/$ref"; + for (var i = 0; i < length; i++) + { + context.Exit(); + } + } + + void EnterSegments(string[] segments) + { + foreach (var segment in segments) + { + context.Enter(segment); + } + } + + IEnumerable GetSegments() + { + foreach (var segment in this.PathString.Substring(2).Split('/')) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER + yield return segment.Replace("~1", "/", StringComparison.OrdinalIgnoreCase).Replace("~0", "~", StringComparison.OrdinalIgnoreCase); #else - PathString.Substring(2) + "/$ref"; + yield return segment.Replace("~1", "/").Replace("~0", "~"); #endif + } + yield return "$ref"; + // Trim off the leading "#/" as the context is already at the root of the document } } } From bdb6c0d2b6b0eae4c0af59a27e3e964d0f84ca02 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 17 Mar 2026 16:34:21 -0400 Subject: [PATCH 6/6] fix missing encoding for invalid request body rule Signed-off-by: Vincent Biret --- .../Validations/OpenApiRecommendedRulesTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs index c5aa3533d..13d710909 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs @@ -200,7 +200,7 @@ public static void GetOperationWithRequestBodyIsInvalid() Assert.NotNull(warnings); var warning = Assert.Single(warnings); Assert.Equal("GET operations should not have a request body.", warning.Message); - Assert.Equal("#/paths//people/get/requestBody", warning.Pointer); + Assert.Equal("#/paths/~1people/get/requestBody", warning.Pointer); } [Fact]