diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index b375a58..4317416 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -5,6 +5,38 @@ module SchemaReader = open System.IO open System.Net 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 + 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 /../ 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(startsWithRelativeMarker path) let getAbsolutePath (resolutionFolder: string) (schemaPathRaw: string) = if String.IsNullOrWhiteSpace(schemaPathRaw) then @@ -14,8 +46,16 @@ 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..669aca1 --- /dev/null +++ b/tests/SwaggerProvider.Tests/PathResolutionTests.fs @@ -0,0 +1,127 @@ +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 @@ +