Skip to content

Commit e540a0b

Browse files
committed
(139379934) URL should not strip trailing slash for root paths on Windows
1 parent a9dc42c commit e540a0b

File tree

2 files changed

+49
-18
lines changed

2 files changed

+49
-18
lines changed

Sources/FoundationEssentials/URL/URL.swift

+22-17
Original file line numberDiff line numberDiff line change
@@ -1349,31 +1349,36 @@ public struct URL: Equatable, Sendable, Hashable {
13491349
}
13501350
}
13511351

1352-
private static func windowsPath(for posixPath: String) -> String {
1353-
let utf8 = posixPath.utf8
1354-
guard utf8.count >= 4 else {
1355-
return posixPath
1352+
private static func windowsPath(for urlPath: String) -> String {
1353+
let utf8 = urlPath.utf8
1354+
guard !utf8.starts(with: [._slash, ._slash]) else {
1355+
// UNC path, don't strip any trailing slash, which might be root
1356+
return decodeFilePath(urlPath)
13561357
}
13571358
// "C:\" is standardized to "/C:/" on initialization
1358-
let array = Array(utf8)
1359-
if array[0] == ._slash,
1360-
array[1].isAlpha,
1361-
array[2] == ._colon,
1362-
array[3] == ._slash {
1363-
return String(Substring(utf8.dropFirst()))
1359+
var iter = utf8.makeIterator()
1360+
guard iter.next() == ._slash,
1361+
let driveLetter = iter.next(), driveLetter.isAlpha,
1362+
iter.next() == ._colon,
1363+
iter.next() == ._slash else {
1364+
return decodeFilePath(urlPath._droppingTrailingSlashes)
13641365
}
1365-
return posixPath
1366+
// Strip trailing slashes from the path, which preserves a root "/"
1367+
let path = String(Substring(utf8.dropFirst(3)))._droppingTrailingSlashes
1368+
// Don't include a leading slash before the drive letter
1369+
return "\(Unicode.Scalar(driveLetter)):\(decodeFilePath(path))"
13661370
}
13671371

1368-
private static func fileSystemPath(for urlPath: String) -> String {
1372+
private static func decodeFilePath(_ path: some StringProtocol) -> String {
13691373
let charsToLeaveEncoded: Set<UInt8> = [._slash, 0]
1370-
guard let posixPath = Parser.percentDecode(urlPath._droppingTrailingSlashes, excluding: charsToLeaveEncoded) else {
1371-
return ""
1372-
}
1374+
return Parser.percentDecode(path, excluding: charsToLeaveEncoded) ?? ""
1375+
}
1376+
1377+
private static func fileSystemPath(for urlPath: String) -> String {
13731378
#if os(Windows)
1374-
return windowsPath(for: posixPath)
1379+
return windowsPath(for: urlPath)
13751380
#else
1376-
return posixPath
1381+
return decodeFilePath(urlPath._droppingTrailingSlashes)
13771382
#endif
13781383
}
13791384

Tests/FoundationEssentialsTests/URLTests.swift

+27-1
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,39 @@ final class URLTests : XCTestCase {
340340

341341
#if os(Windows)
342342
func testURLWindowsDriveLetterPath() throws {
343-
let url = URL(filePath: "C:\\test\\path", directoryHint: .notDirectory)
343+
var url = URL(filePath: #"C:\test\path"#, directoryHint: .notDirectory)
344344
// .absoluteString and .path() use the RFC 8089 URL path
345345
XCTAssertEqual(url.absoluteString, "file:///C:/test/path")
346346
XCTAssertEqual(url.path(), "/C:/test/path")
347347
// .path and .fileSystemPath strip the leading slash
348348
XCTAssertEqual(url.path, "C:/test/path")
349349
XCTAssertEqual(url.fileSystemPath, "C:/test/path")
350+
351+
url = URL(filePath: #"C:\"#, directoryHint: .isDirectory)
352+
XCTAssertEqual(url.absoluteString, "file:///C:/")
353+
XCTAssertEqual(url.path(), "/C:/")
354+
XCTAssertEqual(url.path, "C:/")
355+
XCTAssertEqual(url.fileSystemPath, "C:/")
356+
357+
url = URL(filePath: #"C:\\\"#, directoryHint: .isDirectory)
358+
XCTAssertEqual(url.absoluteString, "file:///C:///")
359+
XCTAssertEqual(url.path(), "/C:///")
360+
XCTAssertEqual(url.path, "C:/")
361+
XCTAssertEqual(url.fileSystemPath, "C:/")
362+
363+
url = URL(filePath: #"\C:\"#, directoryHint: .isDirectory)
364+
XCTAssertEqual(url.absoluteString, "file:///C:/")
365+
XCTAssertEqual(url.path(), "/C:/")
366+
XCTAssertEqual(url.path, "C:/")
367+
XCTAssertEqual(url.fileSystemPath, "C:/")
368+
369+
let base = URL(filePath: #"\d:\path\"#, directoryHint: .isDirectory)
370+
url = URL(filePath: #"%43:\fake\letter"#, directoryHint: .notDirectory, relativeTo: base)
371+
// ":" is encoded to "%3A" in the first path segment so it's not mistaken as the scheme separator
372+
XCTAssertEqual(url.relativeString, "%2543%3A/fake/letter")
373+
XCTAssertEqual(url.path(), "/d:/path/%2543%3A/fake/letter")
374+
XCTAssertEqual(url.path, "d:/path/%43:/fake/letter")
375+
XCTAssertEqual(url.fileSystemPath, "d:/path/%43:/fake/letter")
350376
}
351377
#endif
352378

0 commit comments

Comments
 (0)