Skip to content
Merged
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
44 changes: 42 additions & 2 deletions src/SwaggerProvider.DesignTime/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
127 changes: 127 additions & 0 deletions tests/SwaggerProvider.Tests/PathResolutionTests.fs
Original file line number Diff line number Diff line change
@@ -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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)
1 change: 1 addition & 0 deletions tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Compile Include="v2\Schema.Spec.Yaml.Tests.fs" />
<Compile Include="APIs.guru.fs" />
<Compile Include="Schema.Parser.Tests.fs" />
<Compile Include="PathResolutionTests.fs" />
<Compile Include="SsrfSecurityTests.fs" />
<None Include="paket.references" />
<ProjectReference Include="..\..\src\SwaggerProvider.DesignTime\SwaggerProvider.DesignTime.fsproj" />
Expand Down
Loading