From 3513325cc015a006fe7cdadfb8ae2db3c4ac7fec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:32:56 +0000 Subject: [PATCH 1/5] Initial plan From 6bc63fdf222db26714477850a1294f2c62874b95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:42:41 +0000 Subject: [PATCH 2/5] Fix relative file path verification on Windows - Add isTrulyAbsolute helper to distinguish between truly absolute paths and rooted relative paths - On Windows, paths starting with / or \ followed by .. or . are now correctly treated as relative - On Unix, paths starting with /../ or /./ are also correctly treated as relative - Add comprehensive unit tests for path resolution logic - All existing tests still pass Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.DesignTime/Utils.fs | 38 ++++++- .../PathResolutionTests.fs | 103 ++++++++++++++++++ .../SwaggerProvider.Tests.fsproj | 1 + 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/SwaggerProvider.Tests/PathResolutionTests.fs diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index b375a58..e9c5414 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -5,6 +5,35 @@ module SchemaReader = open System.IO open System.Net open System.Net.Http + open System.Runtime.InteropServices + + /// Determines if a path is truly absolute (not just rooted) + /// On Windows: C:\path is absolute, \path is rooted (combine with drive), but \..\path is relative + /// On Unix: /path is absolute, but /../path or /./path are relative + let private isTrulyAbsolute (path: string) = + if not (Path.IsPathRooted path) then + false + else + let root = Path.GetPathRoot path + if String.IsNullOrEmpty root then + false + else + if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then + // On Windows, a truly absolute path has a volume (C:\, D:\, etc.) + // Paths like \path or /path are rooted but may be relative if they start with .. or . + if root.Contains(':') then + // Has drive letter, truly absolute + true + else + // Rooted but no drive - check if it starts with relative markers + // \..\ or /../ or /..\ etc. are relative, not absolute + // \.\ or /./ or /\ etc. are also relative + let normalized = path.Replace('\\', '/') + not (normalized.StartsWith("/../") || normalized.StartsWith("/./") || normalized.StartsWith("/..\\") || normalized.StartsWith("/.\\")) + else + // On Unix, a rooted path is absolute if it starts with / + // BUT: if the path starts with /../ or /./, it's relative + root = "/" && not (path.StartsWith("/../") || path.StartsWith("/./")) let getAbsolutePath (resolutionFolder: string) (schemaPathRaw: string) = if String.IsNullOrWhiteSpace(schemaPathRaw) then @@ -14,8 +43,13 @@ module SchemaReader = if uri.IsAbsoluteUri then schemaPathRaw - elif Path.IsPathRooted schemaPathRaw then - Path.Combine(Path.GetPathRoot resolutionFolder, schemaPathRaw.Substring 1) + elif isTrulyAbsolute schemaPathRaw then + // Truly absolute path (e.g., C:\path on Windows, /path on Unix) + // On Windows, if path is like \path without drive, combine with drive from resolutionFolder + if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && not (Path.GetPathRoot(schemaPathRaw).Contains(':')) then + Path.Combine(Path.GetPathRoot resolutionFolder, schemaPathRaw.Substring 1) + else + schemaPathRaw else Path.Combine(resolutionFolder, schemaPathRaw) diff --git a/tests/SwaggerProvider.Tests/PathResolutionTests.fs b/tests/SwaggerProvider.Tests/PathResolutionTests.fs new file mode 100644 index 0000000..ff1b308 --- /dev/null +++ b/tests/SwaggerProvider.Tests/PathResolutionTests.fs @@ -0,0 +1,103 @@ +namespace SwaggerProvider.Tests.PathResolutionTests + +open System +open System.IO +open System.Runtime.InteropServices +open Xunit +open SwaggerProvider.Internal.SchemaReader + +/// Tests for path resolution logic +/// These tests verify that relative file paths are handled correctly across platforms +module PathResolutionTests = + + let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + + [] + let ``getAbsolutePath handles paths with parent directory references after concatenation``() = + // Test: When __SOURCE_DIRECTORY__ + "/../Schemas/..." is used, the result should be + // treated as a valid path, not incorrectly parsed + let resolutionFolder = if isWindows then "C:\\Users\\test\\project\\tests" else "/home/user/project/tests" + // Simulate what happens when you do: __SOURCE_DIRECTORY__ + "/../Schemas/..." + let concatenated = resolutionFolder + (if isWindows then "\\..\\Schemas\\v2\\petstore.json" else "/../Schemas/v2/petstore.json") + + let result = getAbsolutePath resolutionFolder concatenated + + // Should keep the path as-is (it's already a full path after concatenation) + // Path.GetFullPath will normalize it later + Assert.Contains("Schemas", result) + Assert.Contains("petstore.json", result) + + [] + let ``getAbsolutePath handles simple relative paths``() = + // Test: Simple relative paths should be combined with resolution folder + let resolutionFolder = if isWindows then "C:\\Users\\test\\project" else "/home/user/project" + let schemaPath = "../Schemas/v2/petstore.json" + + let result = getAbsolutePath resolutionFolder schemaPath + + // Should combine with resolution folder + Assert.Contains("project", result) + Assert.Contains("Schemas", result) + + [] + let ``getAbsolutePath handles current directory relative paths``() = + // Test: Paths starting with ./ should be treated as relative + let resolutionFolder = if isWindows then "C:\\Users\\test\\project" else "/home/user/project" + let schemaPath = "./Schemas/v2/petstore.json" + + let result = getAbsolutePath resolutionFolder schemaPath + + // Should combine with resolution folder + Assert.Contains("project", result) + Assert.Contains("Schemas", result) + + [] + let ``getAbsolutePath handles absolute Unix paths``() = + if not isWindows then + // Test: Absolute Unix paths should be kept as-is + let resolutionFolder = "/home/user/project" + let schemaPath = "/etc/schemas/petstore.json" + + let result = getAbsolutePath resolutionFolder schemaPath + + // Should keep the absolute path + Assert.Equal("/etc/schemas/petstore.json", result) + + [] + let ``getAbsolutePath handles absolute Windows paths with drive letter``() = + if isWindows then + // Test: Absolute Windows paths with drive should be kept as-is + let resolutionFolder = "C:\\Users\\test\\project" + let schemaPath = "D:\\Schemas\\petstore.json" + + let result = getAbsolutePath resolutionFolder schemaPath + + // Should keep the absolute path + Assert.Equal("D:\\Schemas\\petstore.json", result) + + [] + let ``getAbsolutePath handles HTTP URLs``() = + // Test: HTTP URLs should be kept as-is + let resolutionFolder = if isWindows then "C:\\Users\\test\\project" else "/home/user/project" + let schemaPath = "https://example.com/schema.json" + + let result = getAbsolutePath resolutionFolder schemaPath + + // Should keep the URL unchanged + Assert.Equal("https://example.com/schema.json", result) + + [] + let ``getAbsolutePath concatenated with SOURCE_DIRECTORY works correctly``() = + // Test: Simulates the common pattern: __SOURCE_DIRECTORY__ + "/../Schemas/..." + // This should work correctly on both Windows and Unix + let sourceDir = __SOURCE_DIRECTORY__ + let relativePart = "/../Schemas/v2/petstore.json" + let combined = sourceDir + relativePart + + // This simulates what happens in test files + let result = getAbsolutePath sourceDir combined + + // Should resolve to a path that contains Schemas + // The exact result depends on whether the file exists, but it should at least + // not throw an exception and should contain "Schemas" + Assert.Contains("Schemas", result) diff --git a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj index 7ed16ec..d2c7357 100644 --- a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj +++ b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj @@ -16,6 +16,7 @@ + From 88626a52e6282f6640640b6b96a5c8708bc01687 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:43:46 +0000 Subject: [PATCH 3/5] Apply Fantomas formatting to match repository standards Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.DesignTime/Utils.fs | 47 ++++++++------ .../PathResolutionTests.fs | 62 +++++++++++++------ 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index e9c5414..b62bd4d 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -10,30 +10,36 @@ module SchemaReader = /// Determines if a path is truly absolute (not just rooted) /// On Windows: C:\path is absolute, \path is rooted (combine with drive), but \..\path is relative /// On Unix: /path is absolute, but /../path or /./path are relative - let private isTrulyAbsolute (path: string) = - if not (Path.IsPathRooted path) then + let private isTrulyAbsolute(path: string) = + if not(Path.IsPathRooted path) then false else let root = Path.GetPathRoot path + if String.IsNullOrEmpty root then false - else - if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then - // On Windows, a truly absolute path has a volume (C:\, D:\, etc.) - // Paths like \path or /path are rooted but may be relative if they start with .. or . - if root.Contains(':') then - // Has drive letter, truly absolute - true - else - // Rooted but no drive - check if it starts with relative markers - // \..\ or /../ or /..\ etc. are relative, not absolute - // \.\ or /./ or /\ etc. are also relative - let normalized = path.Replace('\\', '/') - not (normalized.StartsWith("/../") || normalized.StartsWith("/./") || normalized.StartsWith("/..\\") || normalized.StartsWith("/.\\")) + else if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then + // On Windows, a truly absolute path has a volume (C:\, D:\, etc.) + // Paths like \path or /path are rooted but may be relative if they start with .. or . + if root.Contains(':') then + // Has drive letter, truly absolute + true else - // On Unix, a rooted path is absolute if it starts with / - // BUT: if the path starts with /../ or /./, it's relative - root = "/" && not (path.StartsWith("/../") || path.StartsWith("/./")) + // Rooted but no drive - check if it starts with relative markers + // \..\ or /../ or /..\ etc. are relative, not absolute + // \.\ or /./ or /\ etc. are also relative + let normalized = path.Replace('\\', '/') + + not( + normalized.StartsWith("/../") + || normalized.StartsWith("/./") + || normalized.StartsWith("/..\\") + || normalized.StartsWith("/.\\") + ) + else + // On Unix, a rooted path is absolute if it starts with / + // BUT: if the path starts with /../ or /./, it's relative + root = "/" && not(path.StartsWith("/../") || path.StartsWith("/./")) let getAbsolutePath (resolutionFolder: string) (schemaPathRaw: string) = if String.IsNullOrWhiteSpace(schemaPathRaw) then @@ -46,7 +52,10 @@ module SchemaReader = elif isTrulyAbsolute schemaPathRaw then // Truly absolute path (e.g., C:\path on Windows, /path on Unix) // On Windows, if path is like \path without drive, combine with drive from resolutionFolder - if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && not (Path.GetPathRoot(schemaPathRaw).Contains(':')) then + if + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && not(Path.GetPathRoot(schemaPathRaw).Contains(':')) + then Path.Combine(Path.GetPathRoot resolutionFolder, schemaPathRaw.Substring 1) else schemaPathRaw diff --git a/tests/SwaggerProvider.Tests/PathResolutionTests.fs b/tests/SwaggerProvider.Tests/PathResolutionTests.fs index ff1b308..669aca1 100644 --- a/tests/SwaggerProvider.Tests/PathResolutionTests.fs +++ b/tests/SwaggerProvider.Tests/PathResolutionTests.fs @@ -16,12 +16,21 @@ module PathResolutionTests = let ``getAbsolutePath handles paths with parent directory references after concatenation``() = // Test: When __SOURCE_DIRECTORY__ + "/../Schemas/..." is used, the result should be // treated as a valid path, not incorrectly parsed - let resolutionFolder = if isWindows then "C:\\Users\\test\\project\\tests" else "/home/user/project/tests" + let resolutionFolder = + if isWindows then + "C:\\Users\\test\\project\\tests" + else + "/home/user/project/tests" // Simulate what happens when you do: __SOURCE_DIRECTORY__ + "/../Schemas/..." - let concatenated = resolutionFolder + (if isWindows then "\\..\\Schemas\\v2\\petstore.json" else "/../Schemas/v2/petstore.json") - + let concatenated = + resolutionFolder + + (if isWindows then + "\\..\\Schemas\\v2\\petstore.json" + else + "/../Schemas/v2/petstore.json") + let result = getAbsolutePath resolutionFolder concatenated - + // Should keep the path as-is (it's already a full path after concatenation) // Path.GetFullPath will normalize it later Assert.Contains("Schemas", result) @@ -30,11 +39,16 @@ module PathResolutionTests = [] let ``getAbsolutePath handles simple relative paths``() = // Test: Simple relative paths should be combined with resolution folder - let resolutionFolder = if isWindows then "C:\\Users\\test\\project" else "/home/user/project" + let resolutionFolder = + if isWindows then + "C:\\Users\\test\\project" + else + "/home/user/project" + let schemaPath = "../Schemas/v2/petstore.json" - + let result = getAbsolutePath resolutionFolder schemaPath - + // Should combine with resolution folder Assert.Contains("project", result) Assert.Contains("Schemas", result) @@ -42,11 +56,16 @@ module PathResolutionTests = [] let ``getAbsolutePath handles current directory relative paths``() = // Test: Paths starting with ./ should be treated as relative - let resolutionFolder = if isWindows then "C:\\Users\\test\\project" else "/home/user/project" + let resolutionFolder = + if isWindows then + "C:\\Users\\test\\project" + else + "/home/user/project" + let schemaPath = "./Schemas/v2/petstore.json" - + let result = getAbsolutePath resolutionFolder schemaPath - + // Should combine with resolution folder Assert.Contains("project", result) Assert.Contains("Schemas", result) @@ -57,9 +76,9 @@ module PathResolutionTests = // Test: Absolute Unix paths should be kept as-is let resolutionFolder = "/home/user/project" let schemaPath = "/etc/schemas/petstore.json" - + let result = getAbsolutePath resolutionFolder schemaPath - + // Should keep the absolute path Assert.Equal("/etc/schemas/petstore.json", result) @@ -69,20 +88,25 @@ module PathResolutionTests = // Test: Absolute Windows paths with drive should be kept as-is let resolutionFolder = "C:\\Users\\test\\project" let schemaPath = "D:\\Schemas\\petstore.json" - + let result = getAbsolutePath resolutionFolder schemaPath - + // Should keep the absolute path Assert.Equal("D:\\Schemas\\petstore.json", result) [] let ``getAbsolutePath handles HTTP URLs``() = // Test: HTTP URLs should be kept as-is - let resolutionFolder = if isWindows then "C:\\Users\\test\\project" else "/home/user/project" + let resolutionFolder = + if isWindows then + "C:\\Users\\test\\project" + else + "/home/user/project" + let schemaPath = "https://example.com/schema.json" - + let result = getAbsolutePath resolutionFolder schemaPath - + // Should keep the URL unchanged Assert.Equal("https://example.com/schema.json", result) @@ -93,10 +117,10 @@ module PathResolutionTests = let sourceDir = __SOURCE_DIRECTORY__ let relativePart = "/../Schemas/v2/petstore.json" let combined = sourceDir + relativePart - + // This simulates what happens in test files let result = getAbsolutePath sourceDir combined - + // Should resolve to a path that contains Schemas // The exact result depends on whether the file exists, but it should at least // not throw an exception and should contain "Schemas" From 8bd31135c641aa37743d4e5ab2e7b7d1e91fa311 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:45:28 +0000 Subject: [PATCH 4/5] Refactor relative path checking logic to remove redundancy - Extract startsWithRelativeMarker helper function to avoid code duplication - Remove redundant checks after path normalization - Both Windows and Unix platforms now use the same helper for consistency Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.DesignTime/Utils.fs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index b62bd4d..84cf8ac 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -7,6 +7,11 @@ module SchemaReader = open System.Net.Http open System.Runtime.InteropServices + /// Checks if a path starts with relative markers like ../ or ./ + let private startsWithRelativeMarker(path: string) = + let normalized = path.Replace('\\', '/') + normalized.StartsWith("/../") || normalized.StartsWith("/./") + /// Determines if a path is truly absolute (not just rooted) /// On Windows: C:\path is absolute, \path is rooted (combine with drive), but \..\path is relative /// On Unix: /path is absolute, but /../path or /./path are relative @@ -26,20 +31,12 @@ module SchemaReader = true else // Rooted but no drive - check if it starts with relative markers - // \..\ or /../ or /..\ etc. are relative, not absolute - // \.\ or /./ or /\ etc. are also relative - let normalized = path.Replace('\\', '/') - - not( - normalized.StartsWith("/../") - || normalized.StartsWith("/./") - || normalized.StartsWith("/..\\") - || normalized.StartsWith("/.\\") - ) + // \..\ or /../ are relative, not absolute + not(startsWithRelativeMarker path) else // On Unix, a rooted path is absolute if it starts with / // BUT: if the path starts with /../ or /./, it's relative - root = "/" && not(path.StartsWith("/../") || path.StartsWith("/./")) + root = "/" && not(startsWithRelativeMarker path) let getAbsolutePath (resolutionFolder: string) (schemaPathRaw: string) = if String.IsNullOrWhiteSpace(schemaPathRaw) then From 3e1ed8b0dfa68d4341d194666ce5526474446c8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:02:40 +0000 Subject: [PATCH 5/5] Fix netstandard2.0 compatibility by using string overload for Contains Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.DesignTime/Utils.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index 84cf8ac..4317416 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -26,7 +26,7 @@ module SchemaReader = else if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then // On Windows, a truly absolute path has a volume (C:\, D:\, etc.) // Paths like \path or /path are rooted but may be relative if they start with .. or . - if root.Contains(':') then + if root.Contains(":") then // Has drive letter, truly absolute true else @@ -51,7 +51,7 @@ module SchemaReader = // On Windows, if path is like \path without drive, combine with drive from resolutionFolder if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - && not(Path.GetPathRoot(schemaPathRaw).Contains(':')) + && not(Path.GetPathRoot(schemaPathRaw).Contains(":")) then Path.Combine(Path.GetPathRoot resolutionFolder, schemaPathRaw.Substring 1) else