Skip to content

Commit e991656

Browse files
authored
URL.init(filePath:) should resolve against the base URL before checking if the file is a directory (#606)
1 parent 8bdf040 commit e991656

File tree

3 files changed

+42
-10
lines changed

3 files changed

+42
-10
lines changed

Sources/FoundationEssentials/String/String+Path.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ extension String {
199199
return result
200200
}
201201

202+
internal func merging(relativePath: String) -> String {
203+
guard relativePath.utf8.first != UInt8(ascii: "/") else {
204+
return relativePath
205+
}
206+
guard let basePathEnd = self.utf8.lastIndex(of: UInt8(ascii: "/")) else {
207+
return relativePath
208+
}
209+
return self[...basePathEnd] + relativePath
210+
}
211+
202212
internal var removingDotSegments: String {
203213
let input = self.utf8
204214
guard !input.isEmpty else {

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,12 +1011,11 @@ public struct URL: Equatable, Sendable, Hashable {
10111011
guard let _baseParseInfo else {
10121012
return relativePath
10131013
}
1014-
let basePath = _baseParseInfo.path
1014+
let basePath = String(_baseParseInfo.path)
10151015
if _baseParseInfo.hasAuthority && basePath.isEmpty {
10161016
return "/" + relativePath
10171017
}
1018-
let basePathEnd = basePath.utf8.lastIndex(of: UInt8(ascii: "/")) ?? basePath.startIndex
1019-
return basePath[..<basePathEnd] + "/" + relativePath
1018+
return basePath.merging(relativePath: relativePath)
10201019
}
10211020

10221021
/// Calculate the "merged" path that is resovled against the base URL
@@ -1295,7 +1294,7 @@ public struct URL: Equatable, Sendable, Hashable {
12951294
}
12961295
}
12971296

1298-
private func fileSystemPath(for urlPath: String) -> String {
1297+
private static func fileSystemPath(for urlPath: String) -> String {
12991298
var result = urlPath
13001299
if result.count > 1 && result.utf8.last == UInt8(ascii: "/") {
13011300
_ = result.popLast()
@@ -1305,7 +1304,7 @@ public struct URL: Equatable, Sendable, Hashable {
13051304
}
13061305

13071306
var fileSystemPath: String {
1308-
return fileSystemPath(for: path())
1307+
return URL.fileSystemPath(for: path())
13091308
}
13101309

13111310
/// Returns the path component of the URL if present, otherwise returns an empty string.
@@ -1382,7 +1381,7 @@ public struct URL: Equatable, Sendable, Hashable {
13821381
}
13831382
}
13841383
#endif
1385-
return fileSystemPath(for: relativePath())
1384+
return URL.fileSystemPath(for: relativePath())
13861385
}
13871386

13881387
private func relativePath(percentEncoded: Bool = true) -> String {
@@ -2098,11 +2097,12 @@ extension URL {
20982097
guard !isAbsolute, let baseURL else {
20992098
return filePath
21002099
}
2100+
let basePath = baseURL.path()
21012101
#if os(Windows)
21022102
let urlPath = filePath.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/"))
2103-
return baseURL.mergedPath(for: urlPath).replacing(UInt8(ascii: "/"), with: UInt8(ascii: "\\"))
2103+
return URL.fileSystemPath(for: basePath.merging(relativePath: urlPath)).replacing(UInt8(ascii: "/"), with: UInt8(ascii: "\\"))
21042104
#else
2105-
return baseURL.mergedPath(for: filePath)
2105+
return URL.fileSystemPath(for: basePath.merging(relativePath: filePath))
21062106
#endif
21072107
}
21082108

@@ -2188,9 +2188,9 @@ extension URL {
21882188
if isFileURL {
21892189
let filePath: String
21902190
if newPath.utf8.first == slash {
2191-
filePath = fileSystemPath(for: newPath)
2191+
filePath = URL.fileSystemPath(for: newPath)
21922192
} else {
2193-
filePath = fileSystemPath(for: mergedPath(for: newPath))
2193+
filePath = URL.fileSystemPath(for: mergedPath(for: newPath))
21942194
}
21952195
isDirectory = URL.isDirectory(filePath)
21962196
} else {

Tests/FoundationEssentialsTests/URLTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,28 @@ final class URLTests : XCTestCase {
330330
try FileManager.default.removeItem(at: URL(filePath: "\(tempDirectory.path)/tmp-dir"))
331331
}
332332

333+
func testURLFilePathRelativeToBase() throws {
334+
try FileManagerPlayground {
335+
Directory("dir") {
336+
"Foo"
337+
"Bar"
338+
}
339+
}.test {
340+
let currentDirectoryPath = $0.currentDirectoryPath
341+
let baseURL = URL(filePath: currentDirectoryPath, directoryHint: .isDirectory)
342+
let relativePath = "dir"
343+
344+
let url1 = URL(filePath: relativePath, directoryHint: .isDirectory, relativeTo: baseURL)
345+
346+
let url2 = URL(filePath: relativePath, directoryHint: .checkFileSystem, relativeTo: baseURL)
347+
XCTAssertEqual(url1, url2, "\(url1) was not equal to \(url2)")
348+
349+
// directoryHint is `.inferFromPath` by default
350+
let url3 = URL(filePath: relativePath + "/", relativeTo: baseURL)
351+
XCTAssertEqual(url1, url3, "\(url1) was not equal to \(url3)")
352+
}
353+
}
354+
333355
func testAppendFamily() throws {
334356
let base = URL(string: "https://www.example.com")!
335357

0 commit comments

Comments
 (0)