Skip to content

Commit ed3414f

Browse files
fix: relative file path verification on Windows (#281)
* Initial plan * 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 <[email protected]> * Apply Fantomas formatting to match repository standards Co-authored-by: sergey-tihon <[email protected]> * 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 <[email protected]> * Fix netstandard2.0 compatibility by using string overload for Contains Co-authored-by: sergey-tihon <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: sergey-tihon <[email protected]>
1 parent 669ac1b commit ed3414f

File tree

3 files changed

+170
-2
lines changed

3 files changed

+170
-2
lines changed

src/SwaggerProvider.DesignTime/Utils.fs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ module SchemaReader =
55
open System.IO
66
open System.Net
77
open System.Net.Http
8+
open System.Runtime.InteropServices
9+
10+
/// Checks if a path starts with relative markers like ../ or ./
11+
let private startsWithRelativeMarker(path: string) =
12+
let normalized = path.Replace('\\', '/')
13+
normalized.StartsWith("/../") || normalized.StartsWith("/./")
14+
15+
/// Determines if a path is truly absolute (not just rooted)
16+
/// On Windows: C:\path is absolute, \path is rooted (combine with drive), but \..\path is relative
17+
/// On Unix: /path is absolute, but /../path or /./path are relative
18+
let private isTrulyAbsolute(path: string) =
19+
if not(Path.IsPathRooted path) then
20+
false
21+
else
22+
let root = Path.GetPathRoot path
23+
24+
if String.IsNullOrEmpty root then
25+
false
26+
else if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then
27+
// On Windows, a truly absolute path has a volume (C:\, D:\, etc.)
28+
// Paths like \path or /path are rooted but may be relative if they start with .. or .
29+
if root.Contains(":") then
30+
// Has drive letter, truly absolute
31+
true
32+
else
33+
// Rooted but no drive - check if it starts with relative markers
34+
// \..\ or /../ are relative, not absolute
35+
not(startsWithRelativeMarker path)
36+
else
37+
// On Unix, a rooted path is absolute if it starts with /
38+
// BUT: if the path starts with /../ or /./, it's relative
39+
root = "/" && not(startsWithRelativeMarker path)
840

941
let getAbsolutePath (resolutionFolder: string) (schemaPathRaw: string) =
1042
if String.IsNullOrWhiteSpace(schemaPathRaw) then
@@ -14,8 +46,16 @@ module SchemaReader =
1446

1547
if uri.IsAbsoluteUri then
1648
schemaPathRaw
17-
elif Path.IsPathRooted schemaPathRaw then
18-
Path.Combine(Path.GetPathRoot resolutionFolder, schemaPathRaw.Substring 1)
49+
elif isTrulyAbsolute schemaPathRaw then
50+
// Truly absolute path (e.g., C:\path on Windows, /path on Unix)
51+
// On Windows, if path is like \path without drive, combine with drive from resolutionFolder
52+
if
53+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
54+
&& not(Path.GetPathRoot(schemaPathRaw).Contains(":"))
55+
then
56+
Path.Combine(Path.GetPathRoot resolutionFolder, schemaPathRaw.Substring 1)
57+
else
58+
schemaPathRaw
1959
else
2060
Path.Combine(resolutionFolder, schemaPathRaw)
2161

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
namespace SwaggerProvider.Tests.PathResolutionTests
2+
3+
open System
4+
open System.IO
5+
open System.Runtime.InteropServices
6+
open Xunit
7+
open SwaggerProvider.Internal.SchemaReader
8+
9+
/// Tests for path resolution logic
10+
/// These tests verify that relative file paths are handled correctly across platforms
11+
module PathResolutionTests =
12+
13+
let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
14+
15+
[<Fact>]
16+
let ``getAbsolutePath handles paths with parent directory references after concatenation``() =
17+
// Test: When __SOURCE_DIRECTORY__ + "/../Schemas/..." is used, the result should be
18+
// treated as a valid path, not incorrectly parsed
19+
let resolutionFolder =
20+
if isWindows then
21+
"C:\\Users\\test\\project\\tests"
22+
else
23+
"/home/user/project/tests"
24+
// Simulate what happens when you do: __SOURCE_DIRECTORY__ + "/../Schemas/..."
25+
let concatenated =
26+
resolutionFolder
27+
+ (if isWindows then
28+
"\\..\\Schemas\\v2\\petstore.json"
29+
else
30+
"/../Schemas/v2/petstore.json")
31+
32+
let result = getAbsolutePath resolutionFolder concatenated
33+
34+
// Should keep the path as-is (it's already a full path after concatenation)
35+
// Path.GetFullPath will normalize it later
36+
Assert.Contains("Schemas", result)
37+
Assert.Contains("petstore.json", result)
38+
39+
[<Fact>]
40+
let ``getAbsolutePath handles simple relative paths``() =
41+
// Test: Simple relative paths should be combined with resolution folder
42+
let resolutionFolder =
43+
if isWindows then
44+
"C:\\Users\\test\\project"
45+
else
46+
"/home/user/project"
47+
48+
let schemaPath = "../Schemas/v2/petstore.json"
49+
50+
let result = getAbsolutePath resolutionFolder schemaPath
51+
52+
// Should combine with resolution folder
53+
Assert.Contains("project", result)
54+
Assert.Contains("Schemas", result)
55+
56+
[<Fact>]
57+
let ``getAbsolutePath handles current directory relative paths``() =
58+
// Test: Paths starting with ./ should be treated as relative
59+
let resolutionFolder =
60+
if isWindows then
61+
"C:\\Users\\test\\project"
62+
else
63+
"/home/user/project"
64+
65+
let schemaPath = "./Schemas/v2/petstore.json"
66+
67+
let result = getAbsolutePath resolutionFolder schemaPath
68+
69+
// Should combine with resolution folder
70+
Assert.Contains("project", result)
71+
Assert.Contains("Schemas", result)
72+
73+
[<Fact>]
74+
let ``getAbsolutePath handles absolute Unix paths``() =
75+
if not isWindows then
76+
// Test: Absolute Unix paths should be kept as-is
77+
let resolutionFolder = "/home/user/project"
78+
let schemaPath = "/etc/schemas/petstore.json"
79+
80+
let result = getAbsolutePath resolutionFolder schemaPath
81+
82+
// Should keep the absolute path
83+
Assert.Equal("/etc/schemas/petstore.json", result)
84+
85+
[<Fact>]
86+
let ``getAbsolutePath handles absolute Windows paths with drive letter``() =
87+
if isWindows then
88+
// Test: Absolute Windows paths with drive should be kept as-is
89+
let resolutionFolder = "C:\\Users\\test\\project"
90+
let schemaPath = "D:\\Schemas\\petstore.json"
91+
92+
let result = getAbsolutePath resolutionFolder schemaPath
93+
94+
// Should keep the absolute path
95+
Assert.Equal("D:\\Schemas\\petstore.json", result)
96+
97+
[<Fact>]
98+
let ``getAbsolutePath handles HTTP URLs``() =
99+
// Test: HTTP URLs should be kept as-is
100+
let resolutionFolder =
101+
if isWindows then
102+
"C:\\Users\\test\\project"
103+
else
104+
"/home/user/project"
105+
106+
let schemaPath = "https://example.com/schema.json"
107+
108+
let result = getAbsolutePath resolutionFolder schemaPath
109+
110+
// Should keep the URL unchanged
111+
Assert.Equal("https://example.com/schema.json", result)
112+
113+
[<Fact>]
114+
let ``getAbsolutePath concatenated with SOURCE_DIRECTORY works correctly``() =
115+
// Test: Simulates the common pattern: __SOURCE_DIRECTORY__ + "/../Schemas/..."
116+
// This should work correctly on both Windows and Unix
117+
let sourceDir = __SOURCE_DIRECTORY__
118+
let relativePart = "/../Schemas/v2/petstore.json"
119+
let combined = sourceDir + relativePart
120+
121+
// This simulates what happens in test files
122+
let result = getAbsolutePath sourceDir combined
123+
124+
// Should resolve to a path that contains Schemas
125+
// The exact result depends on whether the file exists, but it should at least
126+
// not throw an exception and should contain "Schemas"
127+
Assert.Contains("Schemas", result)

tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Compile Include="v2\Schema.Spec.Yaml.Tests.fs" />
1717
<Compile Include="APIs.guru.fs" />
1818
<Compile Include="Schema.Parser.Tests.fs" />
19+
<Compile Include="PathResolutionTests.fs" />
1920
<Compile Include="SsrfSecurityTests.fs" />
2021
<None Include="paket.references" />
2122
<ProjectReference Include="..\..\src\SwaggerProvider.DesignTime\SwaggerProvider.DesignTime.fsproj" />

0 commit comments

Comments
 (0)